+
\ No newline at end of file
diff --git a/.idea/shelf/Update_at_2022_5_5_22_17__Default_Changelist_.xml b/.idea/shelf/Update_at_2022_5_5_22_17__Default_Changelist_.xml
new file mode 100644
index 0000000..c530e64
--- /dev/null
+++ b/.idea/shelf/Update_at_2022_5_5_22_17__Default_Changelist_.xml
@@ -0,0 +1,4 @@
+
+
+
+
+
+@@ -234,26 +242,26 @@
+
+
+
+-
++
+
+
+
+-
+-
++
++
+
+
+
+-
+-
++
++
+
+
+
+-
+-
++
++
+
+
+
+-
++
+
+
+
+@@ -283,11 +291,11 @@
+
+
+
+-
++
+
+
+
+-
++
+
+
+
+@@ -304,9 +312,17 @@
+
+
+
++
++
++
++
+
+
+
+
++
++
++
++
+
+
+\ No newline at end of file
diff --git a/Apps/comments/models.py b/Apps/comments/models.py
index 5bfc434..c6c1df2 100644
--- a/Apps/comments/models.py
+++ b/Apps/comments/models.py
@@ -4,6 +4,8 @@ from django.db import models
class Comments(models.Model):
+ class Meta:
+ verbose_name_plural=u"弹幕内容"
post_time = models.DateTimeField(verbose_name="发布时间")
content = models.CharField(verbose_name="弹幕内容", max_length=50, blank=False)
diff --git a/Apps/comments/serializers.py b/Apps/comments/serializers.py
index 9d57096..268ab32 100644
--- a/Apps/comments/serializers.py
+++ b/Apps/comments/serializers.py
@@ -1,25 +1,28 @@
from rest_framework import serializers
from .models import *
+from utils.get_error_msg import get_error_msg
class CommentsInfo(serializers.ModelSerializer):
class Meta:
model = Comments
- fields = ['content', 'post_time']
+ fields = ['id', 'content', 'post_time']
post_time = serializers.DateTimeField(label="发布时间", required=False)
content = serializers.CharField(label="弹幕内容", max_length=50, required=True)
+
def validate_content(self, value):
ban = ['!', '@', '#', '$', '%', '^', '&', '*', '(', ')', "_", "-"]
for i in ban:
if i in value:
- raise serializers.ValidationError('非法字符')
+ raise serializers.ValidationError(code='40002', detail={'msg': get_error_msg(40002),
+ "code": '40002'})
if len(value) > 50:
- raise serializers.ValidationError("弹幕过长")
+ raise serializers.ValidationError(code='40003', detail=get_error_msg(40003))
elif len(value) == 0:
- raise serializers.ValidationError("输入不能为空")
+ raise serializers.ValidationError(code='40004', detail=get_error_msg(40004))
return value
diff --git a/Apps/comments/views.py b/Apps/comments/views.py
index 3443607..a587bde 100644
--- a/Apps/comments/views.py
+++ b/Apps/comments/views.py
@@ -7,6 +7,7 @@ from rest_framework.response import Response
from .models import *
from .serializers import CommentsInfo
from django.utils import timezone
+from utils.get_error_msg import get_error_msg
# Create your views here.
@@ -21,11 +22,11 @@ class comments(APIView):
except:
data['msg'] = serializer.error_messages
if len(data['data']) == 0:
- data['msg'] = 'error'
- data['code'] = "40000"
+ data['msg'] = get_error_msg(40005)
+ data['code'] = 40005
else:
- data['msg'] = "success"
- data['code'] = "20000"
+ data['msg'] = get_error_msg(20000)
+ data['code'] = 20000
return Response(data=data)
@@ -34,12 +35,12 @@ class comments(APIView):
serializer = CommentsInfo(data=request.data)
if not serializer.is_valid(raise_exception=True):
data['msg'] = serializer.error_messages
- data['code'] = "40000"
+ data['code'] = 50000
return Response(data=data)
serializer.validated_data['post_time'] = timezone.now().replace(microsecond=0)
serializer.save()
data['data'] = serializer.validated_data
data['msg'] = "success"
- data['code'] = "20000"
+ data['code'] = 20000
return Response(data=data)
diff --git a/Apps/enroll/admin.py b/Apps/enroll/admin.py
index c5dd313..dfebe37 100644
--- a/Apps/enroll/admin.py
+++ b/Apps/enroll/admin.py
@@ -11,13 +11,16 @@ class DepartmentAdmin(admin.ModelAdmin):
list_editable = ('name', 'picture',)
+
list_per_page = 10
+
list_max_show_all = 200 # default
+
search_fields = ['title']
- # date_hierarchy = 'create_date'
+ # date_hierarchy = 'create_date'
'''默认空值'''
empty_value_display = 'NA'
@@ -25,7 +28,6 @@ class DepartmentAdmin(admin.ModelAdmin):
'''过滤选项'''
list_filter = ()
-
class New_memberAdmin(admin.ModelAdmin):
# 定制哪些字段需要展示
list_display = ('id', 'name', 'picture')
@@ -48,7 +50,6 @@ class New_memberAdmin(admin.ModelAdmin):
'''过滤选项'''
list_filter = ()
-
-admin.site.register(Department, DepartmentAdmin)
+admin.site.register(Department ,DepartmentAdmin)
admin.site.register(NewMember)
# admin.site.register(EmailVerifyRecord)
diff --git a/Apps/work/__init__.py b/Apps/work/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/Apps/work/admin.py b/Apps/work/admin.py
new file mode 100644
index 0000000..970b928
--- /dev/null
+++ b/Apps/work/admin.py
@@ -0,0 +1,6 @@
+from django.contrib import admin
+from .models import Works
+# Register your models here.
+
+admin.site.register(Works)
+
diff --git a/Apps/work/apps.py b/Apps/work/apps.py
new file mode 100644
index 0000000..1c07965
--- /dev/null
+++ b/Apps/work/apps.py
@@ -0,0 +1,6 @@
+from django.apps import AppConfig
+
+
+class WorksConfig(AppConfig):
+ default_auto_field = 'django.db.models.BigAutoField'
+ name = 'Apps.work'
diff --git a/Apps/work/migrations/0001_initial.py b/Apps/work/migrations/0001_initial.py
new file mode 100644
index 0000000..ce045ea
--- /dev/null
+++ b/Apps/work/migrations/0001_initial.py
@@ -0,0 +1,24 @@
+# Generated by Django 3.2.5 on 2022-05-05 21:56
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ initial = True
+
+ dependencies = [
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='Works',
+ fields=[
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('grade', models.IntegerField(verbose_name='年份')),
+ ('name', models.CharField(max_length=30, verbose_name='事件名称')),
+ ('description', models.CharField(max_length=200, verbose_name='事件描述')),
+ ('img', models.ImageField(upload_to='image', verbose_name='图片')),
+ ],
+ ),
+ ]
diff --git a/Apps/work/migrations/0002_alter_works_img.py b/Apps/work/migrations/0002_alter_works_img.py
new file mode 100644
index 0000000..d10f54b
--- /dev/null
+++ b/Apps/work/migrations/0002_alter_works_img.py
@@ -0,0 +1,18 @@
+# Generated by Django 3.2.5 on 2022-05-05 21:57
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('work', '0001_initial'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='works',
+ name='img',
+ field=models.ImageField(blank=True, null=True, upload_to='image', verbose_name='图片'),
+ ),
+ ]
diff --git a/Apps/work/migrations/__init__.py b/Apps/work/migrations/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/Apps/work/models.py b/Apps/work/models.py
new file mode 100644
index 0000000..fa4053b
--- /dev/null
+++ b/Apps/work/models.py
@@ -0,0 +1,13 @@
+from django.db import models
+
+# Create your models here.
+
+
+class Works(models.Model):
+ class Meta:
+ verbose_name_plural=u"部门作品"
+
+ grade = models.IntegerField(verbose_name="年份")
+ name = models.CharField(verbose_name="事件名称", max_length=30)
+ description = models.CharField(verbose_name="事件描述", max_length=200)
+ img = models.ImageField(verbose_name="图片", upload_to="image", null=True, blank=True)
diff --git a/Apps/work/serializers.py b/Apps/work/serializers.py
new file mode 100644
index 0000000..b576c73
--- /dev/null
+++ b/Apps/work/serializers.py
@@ -0,0 +1,18 @@
+from rest_framework import serializers
+from .models import *
+
+
+class WorksInfoSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = Works
+ fields = '__all__'
+ grade = serializers.CharField(label="年级", required=True)
+ name = serializers.CharField(label="事件名称", max_length=30, required=True)
+ description = serializers.CharField(label="事件描述", max_length=200, required=True)
+ img = serializers.ImageField(label="图片", required=False)
+
+
+ def validate_grade(self, value):
+ if not (2010 < value <= 2021):
+ raise serializers.ValidationError("不合法输入")
+ return value
\ No newline at end of file
diff --git a/Apps/work/tests.py b/Apps/work/tests.py
new file mode 100644
index 0000000..7ce503c
--- /dev/null
+++ b/Apps/work/tests.py
@@ -0,0 +1,3 @@
+from django.test import TestCase
+
+# Create your tests here.
diff --git a/Apps/work/urls.py b/Apps/work/urls.py
new file mode 100644
index 0000000..e0fdac6
--- /dev/null
+++ b/Apps/work/urls.py
@@ -0,0 +1,13 @@
+from django.contrib import admin
+from django.urls import path
+from . import views
+from django.conf.urls.static import static
+from ITShowPlatform import settings
+
+
+urlpatterns = [
+ path('work/', views.work.as_view()),
+]
+
+urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
+
diff --git a/Apps/work/views.py b/Apps/work/views.py
new file mode 100644
index 0000000..de680cd
--- /dev/null
+++ b/Apps/work/views.py
@@ -0,0 +1,35 @@
+import time
+from django.conf import settings
+import re
+from django.shortcuts import render
+from rest_framework.views import APIView
+from rest_framework.response import Response
+from .models import Works
+from .serializers import WorksInfoSerializer
+# Create your views here.
+
+
+class work(APIView):
+
+ def get(self, request):
+ key = []
+ data = {"data": key}
+ for i in range(2012, 2022):
+ temp = {}
+ try:
+ works_set = Works.objects.filter(grade=i)
+ if works_set:
+ serializer = WorksInfoSerializer(works_set, many=True)
+ temp['grade'] = i
+ temp['data'] = serializer.data
+ data['data'].append(temp)
+ except Exception:
+ pass
+ if len(data['data']) == 0:
+ data['code'] = 40000
+ data['msg'] = "error"
+ else:
+ data['code'] = 20000
+ data['msg'] = 'success'
+ return Response(data=data)
+
diff --git a/ITShowPlatform/settings.py b/ITShowPlatform/settings.py
index 3fabd50..79f7738 100644
--- a/ITShowPlatform/settings.py
+++ b/ITShowPlatform/settings.py
@@ -12,10 +12,16 @@ https://docs.djangoproject.com/en/4.0/ref/settings/
from pathlib import Path
import os
+import configparser
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent
+conf = configparser.RawConfigParser()
+
+conf.read(str(BASE_DIR)+r"\config.ini",encoding="utf-8")
+
+
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/4.0/howto/deployment/checklist/
@@ -29,6 +35,8 @@ ALLOWED_HOSTS = ["*"]
# Application definition
+
+
INSTALLED_APPS = [
'simpleui',
'django.contrib.admin',
@@ -41,7 +49,7 @@ INSTALLED_APPS = [
'Apps.enroll',
'Apps.history',
'Apps.comments',
-
+ 'Apps.work',
]
@@ -81,10 +89,10 @@ WSGI_APPLICATION = 'ITShowPlatform.wsgi.application'
DATABASES = {
'default': {
- 'ENGINE': 'django.db.backends.mysql',
- 'NAME': 'ITShowPlatform',
- 'USER': 'root',
- 'PASSWORD': '123456',
+ 'ENGINE': conf.get("database","ENGINE"),
+ 'NAME': conf.get("database","NAME"),
+ 'USER': conf.get("database","USER"),
+ 'PASSWORD': conf.get("database","PASSWORD"),
}
}
@@ -140,9 +148,10 @@ REST_FRAMEWORK = {
)
}
-EMAIL_HOST = "smtp.qq.com" # 服务器
-EMAIL_PORT = 465
-EMAIL_HOST_USER = "2302253692@qq.com" # 账号
-EMAIL_HOST_PASSWORD = "idujbpdlpgbmdhjg" # 密码 (注意:这里的密码指的是授权码)
-EMAIL_USE_SSL = True # 一般都为False
-EMAIL_FROM = "2302253692@qq.com" # 邮箱来自
+EMAIL_HOST = conf.get('email',"EMAIL_HOST") # 服务器
+EMAIL_PORT = conf.get("email","EMAIL_PORT")
+EMAIL_HOST_USER = conf.get("email","EMAIL_HOST_USER") # 账号
+EMAIL_HOST_PASSWORD = conf.get("email","EMAIL_HOST_PASSWORD") # 密码 (注意:这里的密码指的是授权码)
+EMAIL_USE_SSL = conf.get("email","EMAIL_USE_SSL") # 一般都为False
+EMAIL_FROM = conf.get("email","EMAIL_FROM") # 邮箱来自
+
diff --git a/ITShowPlatform/urls.py b/ITShowPlatform/urls.py
index 1388e13..5a6af80 100644
--- a/ITShowPlatform/urls.py
+++ b/ITShowPlatform/urls.py
@@ -23,6 +23,7 @@ urlpatterns = [
path('v1/api/', include('Apps.comments.urls')),
path('v1/api/', include('Apps.history.urls')),
path('v1/api/', include('Apps.enroll.urls')),
+ path('v1/api/', include('Apps.work.urls')),
path(r'^api-auth/', include('rest_framework.urls')),
re_path(r'^media/(?P.*)', serve, {"document_root": settings.MEDIA_ROOT}),
]
diff --git a/utils/get_error_msg.py b/utils/get_error_msg.py
index b467c22..01262e9 100644
--- a/utils/get_error_msg.py
+++ b/utils/get_error_msg.py
@@ -1,3 +1,4 @@
+
def get_error_msg(code="20000"):
error_set = {
"20000": "成功",
@@ -5,6 +6,7 @@ def get_error_msg(code="20000"):
"50403": "Forbidden",
"40000": "请求方法错误",
"40001": "JSON解析错误",
+
"45030": "信息不存在",
"45032": "邮箱验证码过期",
"44031": "邮箱验证码错误",
@@ -18,5 +20,10 @@ def get_error_msg(code="20000"):
"44033": "请勿频繁发送验证码",
"44036": "请输入正确格式的邮箱",
+ "40002": "非法字符",
+ "40003": "弹幕过长",
+ "40004": "输入不能为空",
+ "40005": "返回评论数为0",
+ "40006": "",
}
return error_set.get(str(code))
diff --git a/venv/Lib/site-packages/Django-3.2.5.dist-info/INSTALLER b/venv/Lib/site-packages/Django-3.2.5.dist-info/INSTALLER
new file mode 100644
index 0000000..a1b589e
--- /dev/null
+++ b/venv/Lib/site-packages/Django-3.2.5.dist-info/INSTALLER
@@ -0,0 +1 @@
+pip
diff --git a/venv/Lib/site-packages/Django-3.2.5.dist-info/REQUESTED b/venv/Lib/site-packages/Django-3.2.5.dist-info/REQUESTED
new file mode 100644
index 0000000..e69de29
diff --git a/venv/Lib/site-packages/Django-3.2.5.dist-info/top_level.txt b/venv/Lib/site-packages/Django-3.2.5.dist-info/top_level.txt
new file mode 100644
index 0000000..d3e4ba5
--- /dev/null
+++ b/venv/Lib/site-packages/Django-3.2.5.dist-info/top_level.txt
@@ -0,0 +1 @@
+django
diff --git a/venv/Lib/site-packages/Markdown-3.3.6.dist-info/INSTALLER b/venv/Lib/site-packages/Markdown-3.3.6.dist-info/INSTALLER
new file mode 100644
index 0000000..a1b589e
--- /dev/null
+++ b/venv/Lib/site-packages/Markdown-3.3.6.dist-info/INSTALLER
@@ -0,0 +1 @@
+pip
diff --git a/venv/Lib/site-packages/Markdown-3.3.6.dist-info/LICENSE.md b/venv/Lib/site-packages/Markdown-3.3.6.dist-info/LICENSE.md
new file mode 100644
index 0000000..2652d97
--- /dev/null
+++ b/venv/Lib/site-packages/Markdown-3.3.6.dist-info/LICENSE.md
@@ -0,0 +1,29 @@
+Copyright 2007, 2008 The Python Markdown Project (v. 1.7 and later)
+Copyright 2004, 2005, 2006 Yuri Takhteyev (v. 0.2-1.6b)
+Copyright 2004 Manfred Stienstra (the original version)
+
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+* Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+* Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+* Neither the name of the Python Markdown Project nor the
+ names of its contributors may be used to endorse or promote products
+ derived from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE PYTHON MARKDOWN PROJECT ''AS IS'' AND ANY
+EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL ANY CONTRIBUTORS TO THE PYTHON MARKDOWN PROJECT
+BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGE.
diff --git a/venv/Lib/site-packages/Markdown-3.3.6.dist-info/METADATA b/venv/Lib/site-packages/Markdown-3.3.6.dist-info/METADATA
new file mode 100644
index 0000000..0dd5837
--- /dev/null
+++ b/venv/Lib/site-packages/Markdown-3.3.6.dist-info/METADATA
@@ -0,0 +1,110 @@
+Metadata-Version: 2.1
+Name: Markdown
+Version: 3.3.6
+Summary: Python implementation of Markdown.
+Home-page: https://Python-Markdown.github.io/
+Author: Manfred Stienstra, Yuri takhteyev and Waylan limberg
+Author-email: python.markdown@gmail.com
+Maintainer: Waylan Limberg
+Maintainer-email: python.markdown@gmail.com
+License: BSD License
+Project-URL: Documentation, https://Python-Markdown.github.io/
+Project-URL: GitHub Project, https://github.com/Python-Markdown/markdown
+Project-URL: Issue Tracker, https://github.com/Python-Markdown/markdown/issues
+Platform: UNKNOWN
+Classifier: Development Status :: 5 - Production/Stable
+Classifier: License :: OSI Approved :: BSD License
+Classifier: Operating System :: OS Independent
+Classifier: Programming Language :: Python
+Classifier: Programming Language :: Python :: 3
+Classifier: Programming Language :: Python :: 3.6
+Classifier: Programming Language :: Python :: 3.7
+Classifier: Programming Language :: Python :: 3.8
+Classifier: Programming Language :: Python :: 3.9
+Classifier: Programming Language :: Python :: 3.10
+Classifier: Programming Language :: Python :: 3 :: Only
+Classifier: Programming Language :: Python :: Implementation :: CPython
+Classifier: Programming Language :: Python :: Implementation :: PyPy
+Classifier: Topic :: Communications :: Email :: Filters
+Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content :: CGI Tools/Libraries
+Classifier: Topic :: Internet :: WWW/HTTP :: Site Management
+Classifier: Topic :: Software Development :: Documentation
+Classifier: Topic :: Software Development :: Libraries :: Python Modules
+Classifier: Topic :: Text Processing :: Filters
+Classifier: Topic :: Text Processing :: Markup :: HTML
+Classifier: Topic :: Text Processing :: Markup :: Markdown
+Requires-Python: >=3.6
+Description-Content-Type: text/markdown
+License-File: LICENSE.md
+Requires-Dist: importlib-metadata (>=4.4) ; python_version < "3.10"
+Provides-Extra: testing
+Requires-Dist: coverage ; extra == 'testing'
+Requires-Dist: pyyaml ; extra == 'testing'
+
+[Python-Markdown][]
+===================
+
+[![Build Status][build-button]][build]
+[![Coverage Status][codecov-button]][codecov]
+[![Latest Version][mdversion-button]][md-pypi]
+[![Python Versions][pyversion-button]][md-pypi]
+[![BSD License][bsdlicense-button]][bsdlicense]
+[![Code of Conduct][codeofconduct-button]][Code of Conduct]
+
+[build-button]: https://github.com/Python-Markdown/markdown/workflows/CI/badge.svg?event=push
+[build]: https://github.com/Python-Markdown/markdown/actions?query=workflow%3ACI+event%3Apush
+[codecov-button]: https://codecov.io/gh/Python-Markdown/markdown/branch/master/graph/badge.svg
+[codecov]: https://codecov.io/gh/Python-Markdown/markdown
+[mdversion-button]: https://img.shields.io/pypi/v/Markdown.svg
+[md-pypi]: https://pypi.org/project/Markdown/
+[pyversion-button]: https://img.shields.io/pypi/pyversions/Markdown.svg
+[bsdlicense-button]: https://img.shields.io/badge/license-BSD-yellow.svg
+[bsdlicense]: https://opensource.org/licenses/BSD-3-Clause
+[codeofconduct-button]: https://img.shields.io/badge/code%20of%20conduct-contributor%20covenant-green.svg?style=flat-square
+[Code of Conduct]: https://github.com/Python-Markdown/markdown/blob/master/CODE_OF_CONDUCT.md
+
+This is a Python implementation of John Gruber's [Markdown][].
+It is almost completely compliant with the reference implementation,
+though there are a few known issues. See [Features][] for information
+on what exactly is supported and what is not. Additional features are
+supported by the [Available Extensions][].
+
+[Python-Markdown]: https://Python-Markdown.github.io/
+[Markdown]: https://daringfireball.net/projects/markdown/
+[Features]: https://Python-Markdown.github.io#Features
+[Available Extensions]: https://Python-Markdown.github.io/extensions
+
+Documentation
+-------------
+
+```bash
+pip install markdown
+```
+```python
+import markdown
+html = markdown.markdown(your_text_string)
+```
+
+For more advanced [installation] and [usage] documentation, see the `docs/` directory
+of the distribution or the project website at .
+
+[installation]: https://python-markdown.github.io/install/
+[usage]: https://python-markdown.github.io/reference/
+
+See the change log at .
+
+Support
+-------
+
+You may report bugs, ask for help, and discuss various other issues on the [bug tracker][].
+
+[bug tracker]: https://github.com/Python-Markdown/markdown/issues
+
+Code of Conduct
+---------------
+
+Everyone interacting in the Python-Markdown project's codebases, issue trackers,
+and mailing lists is expected to follow the [Code of Conduct].
+
+
+
diff --git a/venv/Lib/site-packages/Markdown-3.3.6.dist-info/RECORD b/venv/Lib/site-packages/Markdown-3.3.6.dist-info/RECORD
new file mode 100644
index 0000000..dd376e7
--- /dev/null
+++ b/venv/Lib/site-packages/Markdown-3.3.6.dist-info/RECORD
@@ -0,0 +1,77 @@
+../../Scripts/markdown_py.exe,sha256=vCw3UluhGhDZ5OHeFwbu_i2uuAow6Ag76pfC21_f61A,106375
+Markdown-3.3.6.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
+Markdown-3.3.6.dist-info/LICENSE.md,sha256=bxGTy2NHGOZcOlN9biXr1hSCDsDvaTz8EiSBEmONZNo,1645
+Markdown-3.3.6.dist-info/METADATA,sha256=5gK5efFze8GvYs5GX7G5M597OXkRKG5DtqP8CvscZVQ,4630
+Markdown-3.3.6.dist-info/RECORD,,
+Markdown-3.3.6.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
+Markdown-3.3.6.dist-info/WHEEL,sha256=ewwEueio1C2XeHTvT17n8dZUJgOvyCWCt0WVNLClP9o,92
+Markdown-3.3.6.dist-info/entry_points.txt,sha256=j4jiKg-iwZGImvi8OzotZePWoFbJJ4GrfzDqH03u3SQ,1103
+Markdown-3.3.6.dist-info/top_level.txt,sha256=IAxs8x618RXoH1uCqeLLxXsDefJvE_mIibr_M4sOlyk,9
+markdown/__init__.py,sha256=002-LuHviYzROW2rg_gBGai81nMouUNO9UFj5nSsTSk,2065
+markdown/__main__.py,sha256=JX1057VoovH3NA5uH5nQdQE8b0kXoeT79ZxCzFoL_kg,5803
+markdown/__meta__.py,sha256=IDpR5cdETCEXvY2YxKlRPnIQvdyf5vHJolJNDD4Um9w,1630
+markdown/__pycache__/__init__.cpython-39.pyc,,
+markdown/__pycache__/__main__.cpython-39.pyc,,
+markdown/__pycache__/__meta__.cpython-39.pyc,,
+markdown/__pycache__/blockparser.cpython-39.pyc,,
+markdown/__pycache__/blockprocessors.cpython-39.pyc,,
+markdown/__pycache__/core.cpython-39.pyc,,
+markdown/__pycache__/htmlparser.cpython-39.pyc,,
+markdown/__pycache__/inlinepatterns.cpython-39.pyc,,
+markdown/__pycache__/pep562.cpython-39.pyc,,
+markdown/__pycache__/postprocessors.cpython-39.pyc,,
+markdown/__pycache__/preprocessors.cpython-39.pyc,,
+markdown/__pycache__/serializers.cpython-39.pyc,,
+markdown/__pycache__/test_tools.cpython-39.pyc,,
+markdown/__pycache__/treeprocessors.cpython-39.pyc,,
+markdown/__pycache__/util.cpython-39.pyc,,
+markdown/blockparser.py,sha256=JpBhOokOoBUGCXolftOc5m1hPcR2y9s9hVd9WSuhHzo,4285
+markdown/blockprocessors.py,sha256=LK4mfcgjH8rk3zsyxBzxisxdQjpFj0xkg1LxBtlpLUs,24890
+markdown/core.py,sha256=ZHtqvLdVHOKWIuX_UzdL3rIcxMwji5TC5ZCkV19iM4U,15401
+markdown/extensions/__init__.py,sha256=nw2VtafIf5zHjAcUuykQbaNY6taOmNn7ARn11-Pe080,3661
+markdown/extensions/__pycache__/__init__.cpython-39.pyc,,
+markdown/extensions/__pycache__/abbr.cpython-39.pyc,,
+markdown/extensions/__pycache__/admonition.cpython-39.pyc,,
+markdown/extensions/__pycache__/attr_list.cpython-39.pyc,,
+markdown/extensions/__pycache__/codehilite.cpython-39.pyc,,
+markdown/extensions/__pycache__/def_list.cpython-39.pyc,,
+markdown/extensions/__pycache__/extra.cpython-39.pyc,,
+markdown/extensions/__pycache__/fenced_code.cpython-39.pyc,,
+markdown/extensions/__pycache__/footnotes.cpython-39.pyc,,
+markdown/extensions/__pycache__/legacy_attrs.cpython-39.pyc,,
+markdown/extensions/__pycache__/legacy_em.cpython-39.pyc,,
+markdown/extensions/__pycache__/md_in_html.cpython-39.pyc,,
+markdown/extensions/__pycache__/meta.cpython-39.pyc,,
+markdown/extensions/__pycache__/nl2br.cpython-39.pyc,,
+markdown/extensions/__pycache__/sane_lists.cpython-39.pyc,,
+markdown/extensions/__pycache__/smarty.cpython-39.pyc,,
+markdown/extensions/__pycache__/tables.cpython-39.pyc,,
+markdown/extensions/__pycache__/toc.cpython-39.pyc,,
+markdown/extensions/__pycache__/wikilinks.cpython-39.pyc,,
+markdown/extensions/abbr.py,sha256=5TNU5ml6-H1n-fztEkgUphSTvp5yKCXaiPZMrVuRFvo,3186
+markdown/extensions/admonition.py,sha256=INIecvdzQ7RLmgP8M-N6AZJ5uMd6dBfh9Uj6YibgNLk,5847
+markdown/extensions/attr_list.py,sha256=nhKFY_u6BVyKW2oMUeC4wEjqFNGpDSnNXqaohuF6M7I,5988
+markdown/extensions/codehilite.py,sha256=aEorLnWkEA_zwC2gAoqlR5nb8ZwjiUUFe0bA8bVP0Co,11654
+markdown/extensions/def_list.py,sha256=p-JT64hKqMkfxlmhETMVRPxjrdnBIPDW8k3S05S-qNM,3634
+markdown/extensions/extra.py,sha256=udRN8OvSWcq3UwkPygvsFl1RlCVtCJ-ARVg2IwVH6VY,1831
+markdown/extensions/fenced_code.py,sha256=pRZjVaEh8JZdhyLb2Vy7alfqIQNuPtN2Mzt_Imj2Vm0,7346
+markdown/extensions/footnotes.py,sha256=xvT6etWuTWTHLNHXYQWQGV-35RHTCvH9kBp2xJA6Jdg,15481
+markdown/extensions/legacy_attrs.py,sha256=2EaVQkxQoNnP8_lMPvGRBdNda8L4weUQroiyEuVdS-w,2547
+markdown/extensions/legacy_em.py,sha256=18j4L6zdScy9k18y-U2zaIhYsKVTxCaPurjqLFZmWkI,1582
+markdown/extensions/md_in_html.py,sha256=17w2s-YvjzKPWmng9La6J9-1-h1TWNEFBhVd0pF-j9U,15830
+markdown/extensions/meta.py,sha256=EUfkzM7l7UpH__Or9K3pl8ldVddwndlCZWA3d712RAE,2331
+markdown/extensions/nl2br.py,sha256=wAqTNOuf2L1NzlEvEqoID70n9y-aiYaGLkuyQk3CD0w,783
+markdown/extensions/sane_lists.py,sha256=ZQmCf-247KBexVG0fc62nDvokGkV6W1uavYbieNKSG4,1505
+markdown/extensions/smarty.py,sha256=0padzkVCNACainKw-Xj1S5UfT0125VCTfNejmrCZItA,10238
+markdown/extensions/tables.py,sha256=bicFx_wqhnEx6Y_8MJqA56rh71pt5fOe94oiWbvcobY,7685
+markdown/extensions/toc.py,sha256=Q8YP0DIuyl_B0GfUASYbE4q_B7zFsHS93t5fjiZyJWE,14136
+markdown/extensions/wikilinks.py,sha256=GkgT9BY7b1-qW--dIwFAhC9V20RoeF13b7CFdw_V21Q,2812
+markdown/htmlparser.py,sha256=K3OMq-OU2CTWCMNGPbMGqMmiwZAIrG2lQxuDOJxDjYo,13032
+markdown/inlinepatterns.py,sha256=csrxrPIET_nltn-phz80ObXu5i5oibK68h9ZCWT5eAo,29775
+markdown/pep562.py,sha256=5UkqT7sb-cQufgbOl_jF-RYUVVHS7VThzlMzR9vrd3I,8917
+markdown/postprocessors.py,sha256=NeJyWBqPeDuBBJLTGs5Bfm5oTkUBXk9HWBeQy2_OldI,4262
+markdown/preprocessors.py,sha256=-s8QGHGlX7JAIJTfCivuc-CVwTLWs0IyEU94YUT2IvQ,2742
+markdown/serializers.py,sha256=_wQl-iJrPSUEQ4Q1owWYqN9qceVh6TOlAOH_i44BKAQ,6540
+markdown/test_tools.py,sha256=svokrqFAHJ1H_BiYhEY9kilmk4dZIO3jYJTJcOsphCg,8361
+markdown/treeprocessors.py,sha256=S91w6byWeyBF96q9w8SJ_8UQV8p0UuFJ7Brj6Rw0-y4,15433
+markdown/util.py,sha256=1BKofVbYfqmgAK982UJATA22pj-ee9BcwxaHxB_bOZg,16063
diff --git a/venv/Lib/site-packages/Markdown-3.3.6.dist-info/REQUESTED b/venv/Lib/site-packages/Markdown-3.3.6.dist-info/REQUESTED
new file mode 100644
index 0000000..e69de29
diff --git a/venv/Lib/site-packages/Markdown-3.3.6.dist-info/WHEEL b/venv/Lib/site-packages/Markdown-3.3.6.dist-info/WHEEL
new file mode 100644
index 0000000..5bad85f
--- /dev/null
+++ b/venv/Lib/site-packages/Markdown-3.3.6.dist-info/WHEEL
@@ -0,0 +1,5 @@
+Wheel-Version: 1.0
+Generator: bdist_wheel (0.37.0)
+Root-Is-Purelib: true
+Tag: py3-none-any
+
diff --git a/venv/Lib/site-packages/Markdown-3.3.6.dist-info/entry_points.txt b/venv/Lib/site-packages/Markdown-3.3.6.dist-info/entry_points.txt
new file mode 100644
index 0000000..f49693d
--- /dev/null
+++ b/venv/Lib/site-packages/Markdown-3.3.6.dist-info/entry_points.txt
@@ -0,0 +1,23 @@
+[console_scripts]
+markdown_py = markdown.__main__:run
+
+[markdown.extensions]
+abbr = markdown.extensions.abbr:AbbrExtension
+admonition = markdown.extensions.admonition:AdmonitionExtension
+attr_list = markdown.extensions.attr_list:AttrListExtension
+codehilite = markdown.extensions.codehilite:CodeHiliteExtension
+def_list = markdown.extensions.def_list:DefListExtension
+extra = markdown.extensions.extra:ExtraExtension
+fenced_code = markdown.extensions.fenced_code:FencedCodeExtension
+footnotes = markdown.extensions.footnotes:FootnoteExtension
+legacy_attrs = markdown.extensions.legacy_attrs:LegacyAttrExtension
+legacy_em = markdown.extensions.legacy_em:LegacyEmExtension
+md_in_html = markdown.extensions.md_in_html:MarkdownInHtmlExtension
+meta = markdown.extensions.meta:MetaExtension
+nl2br = markdown.extensions.nl2br:Nl2BrExtension
+sane_lists = markdown.extensions.sane_lists:SaneListExtension
+smarty = markdown.extensions.smarty:SmartyExtension
+tables = markdown.extensions.tables:TableExtension
+toc = markdown.extensions.toc:TocExtension
+wikilinks = markdown.extensions.wikilinks:WikiLinkExtension
+
diff --git a/venv/Lib/site-packages/Markdown-3.3.6.dist-info/top_level.txt b/venv/Lib/site-packages/Markdown-3.3.6.dist-info/top_level.txt
new file mode 100644
index 0000000..0918c97
--- /dev/null
+++ b/venv/Lib/site-packages/Markdown-3.3.6.dist-info/top_level.txt
@@ -0,0 +1 @@
+markdown
diff --git a/venv/Lib/site-packages/MySQLdb/__init__.py b/venv/Lib/site-packages/MySQLdb/__init__.py
new file mode 100644
index 0000000..b567363
--- /dev/null
+++ b/venv/Lib/site-packages/MySQLdb/__init__.py
@@ -0,0 +1,170 @@
+"""
+MySQLdb - A DB API v2.0 compatible interface to MySQL.
+
+This package is a wrapper around _mysql, which mostly implements the
+MySQL C API.
+
+connect() -- connects to server
+
+See the C API specification and the MySQL documentation for more info
+on other items.
+
+For information on how MySQLdb handles type conversion, see the
+MySQLdb.converters module.
+"""
+
+try:
+ from MySQLdb.release import version_info
+ from . import _mysql
+
+ assert version_info == _mysql.version_info
+except Exception:
+ raise ImportError(
+ "this is MySQLdb version {}, but _mysql is version {!r}\n_mysql: {!r}".format(
+ version_info, _mysql.version_info, _mysql.__file__
+ )
+ )
+
+
+from ._mysql import (
+ NotSupportedError,
+ OperationalError,
+ get_client_info,
+ ProgrammingError,
+ Error,
+ InterfaceError,
+ debug,
+ IntegrityError,
+ string_literal,
+ MySQLError,
+ DataError,
+ DatabaseError,
+ InternalError,
+ Warning,
+)
+from MySQLdb.constants import FIELD_TYPE
+from MySQLdb.times import (
+ Date,
+ Time,
+ Timestamp,
+ DateFromTicks,
+ TimeFromTicks,
+ TimestampFromTicks,
+)
+
+threadsafety = 1
+apilevel = "2.0"
+paramstyle = "format"
+
+
+class DBAPISet(frozenset):
+ """A special type of set for which A == x is true if A is a
+ DBAPISet and x is a member of that set."""
+
+ def __eq__(self, other):
+ if isinstance(other, DBAPISet):
+ return not self.difference(other)
+ return other in self
+
+
+STRING = DBAPISet([FIELD_TYPE.ENUM, FIELD_TYPE.STRING, FIELD_TYPE.VAR_STRING])
+BINARY = DBAPISet(
+ [
+ FIELD_TYPE.BLOB,
+ FIELD_TYPE.LONG_BLOB,
+ FIELD_TYPE.MEDIUM_BLOB,
+ FIELD_TYPE.TINY_BLOB,
+ ]
+)
+NUMBER = DBAPISet(
+ [
+ FIELD_TYPE.DECIMAL,
+ FIELD_TYPE.DOUBLE,
+ FIELD_TYPE.FLOAT,
+ FIELD_TYPE.INT24,
+ FIELD_TYPE.LONG,
+ FIELD_TYPE.LONGLONG,
+ FIELD_TYPE.TINY,
+ FIELD_TYPE.YEAR,
+ FIELD_TYPE.NEWDECIMAL,
+ ]
+)
+DATE = DBAPISet([FIELD_TYPE.DATE])
+TIME = DBAPISet([FIELD_TYPE.TIME])
+TIMESTAMP = DBAPISet([FIELD_TYPE.TIMESTAMP, FIELD_TYPE.DATETIME])
+DATETIME = TIMESTAMP
+ROWID = DBAPISet()
+
+
+def test_DBAPISet_set_equality():
+ assert STRING == STRING
+
+
+def test_DBAPISet_set_inequality():
+ assert STRING != NUMBER
+
+
+def test_DBAPISet_set_equality_membership():
+ assert FIELD_TYPE.VAR_STRING == STRING
+
+
+def test_DBAPISet_set_inequality_membership():
+ assert FIELD_TYPE.DATE != STRING
+
+
+def Binary(x):
+ return bytes(x)
+
+
+def Connect(*args, **kwargs):
+ """Factory function for connections.Connection."""
+ from MySQLdb.connections import Connection
+
+ return Connection(*args, **kwargs)
+
+
+connect = Connection = Connect
+
+__all__ = [
+ "BINARY",
+ "Binary",
+ "Connect",
+ "Connection",
+ "DATE",
+ "Date",
+ "Time",
+ "Timestamp",
+ "DateFromTicks",
+ "TimeFromTicks",
+ "TimestampFromTicks",
+ "DataError",
+ "DatabaseError",
+ "Error",
+ "FIELD_TYPE",
+ "IntegrityError",
+ "InterfaceError",
+ "InternalError",
+ "MySQLError",
+ "NUMBER",
+ "NotSupportedError",
+ "DBAPISet",
+ "OperationalError",
+ "ProgrammingError",
+ "ROWID",
+ "STRING",
+ "TIME",
+ "TIMESTAMP",
+ "Warning",
+ "apilevel",
+ "connect",
+ "connections",
+ "constants",
+ "converters",
+ "cursors",
+ "debug",
+ "get_client_info",
+ "paramstyle",
+ "string_literal",
+ "threadsafety",
+ "version_info",
+]
diff --git a/venv/Lib/site-packages/MySQLdb/_exceptions.py b/venv/Lib/site-packages/MySQLdb/_exceptions.py
new file mode 100644
index 0000000..ba35dea
--- /dev/null
+++ b/venv/Lib/site-packages/MySQLdb/_exceptions.py
@@ -0,0 +1,69 @@
+"""Exception classes for _mysql and MySQLdb.
+
+These classes are dictated by the DB API v2.0:
+
+ https://www.python.org/dev/peps/pep-0249/
+"""
+
+
+class MySQLError(Exception):
+ """Exception related to operation with MySQL."""
+
+
+class Warning(Warning, MySQLError):
+ """Exception raised for important warnings like data truncations
+ while inserting, etc."""
+
+
+class Error(MySQLError):
+ """Exception that is the base class of all other error exceptions
+ (not Warning)."""
+
+
+class InterfaceError(Error):
+ """Exception raised for errors that are related to the database
+ interface rather than the database itself."""
+
+
+class DatabaseError(Error):
+ """Exception raised for errors that are related to the
+ database."""
+
+
+class DataError(DatabaseError):
+ """Exception raised for errors that are due to problems with the
+ processed data like division by zero, numeric value out of range,
+ etc."""
+
+
+class OperationalError(DatabaseError):
+ """Exception raised for errors that are related to the database's
+ operation and not necessarily under the control of the programmer,
+ e.g. an unexpected disconnect occurs, the data source name is not
+ found, a transaction could not be processed, a memory allocation
+ error occurred during processing, etc."""
+
+
+class IntegrityError(DatabaseError):
+ """Exception raised when the relational integrity of the database
+ is affected, e.g. a foreign key check fails, duplicate key,
+ etc."""
+
+
+class InternalError(DatabaseError):
+ """Exception raised when the database encounters an internal
+ error, e.g. the cursor is not valid anymore, the transaction is
+ out of sync, etc."""
+
+
+class ProgrammingError(DatabaseError):
+ """Exception raised for programming errors, e.g. table not found
+ or already exists, syntax error in the SQL statement, wrong number
+ of parameters specified, etc."""
+
+
+class NotSupportedError(DatabaseError):
+ """Exception raised in case a method or database API was used
+ which is not supported by the database, e.g. requesting a
+ .rollback() on a connection that does not support transaction or
+ has transactions turned off."""
diff --git a/venv/Lib/site-packages/MySQLdb/_mysql.cp39-win_amd64.pyd b/venv/Lib/site-packages/MySQLdb/_mysql.cp39-win_amd64.pyd
new file mode 100644
index 0000000..c2edd42
Binary files /dev/null and b/venv/Lib/site-packages/MySQLdb/_mysql.cp39-win_amd64.pyd differ
diff --git a/venv/Lib/site-packages/MySQLdb/connections.py b/venv/Lib/site-packages/MySQLdb/connections.py
new file mode 100644
index 0000000..3832466
--- /dev/null
+++ b/venv/Lib/site-packages/MySQLdb/connections.py
@@ -0,0 +1,333 @@
+"""
+This module implements connections for MySQLdb. Presently there is
+only one class: Connection. Others are unlikely. However, you might
+want to make your own subclasses. In most cases, you will probably
+override Connection.default_cursor with a non-standard Cursor class.
+"""
+import re
+
+from . import cursors, _mysql
+from ._exceptions import (
+ Warning,
+ Error,
+ InterfaceError,
+ DataError,
+ DatabaseError,
+ OperationalError,
+ IntegrityError,
+ InternalError,
+ NotSupportedError,
+ ProgrammingError,
+)
+
+# Mapping from MySQL charset name to Python codec name
+_charset_to_encoding = {
+ "utf8mb4": "utf8",
+ "utf8mb3": "utf8",
+ "latin1": "cp1252",
+ "koi8r": "koi8_r",
+ "koi8u": "koi8_u",
+}
+
+re_numeric_part = re.compile(r"^(\d+)")
+
+
+def numeric_part(s):
+ """Returns the leading numeric part of a string.
+
+ >>> numeric_part("20-alpha")
+ 20
+ >>> numeric_part("foo")
+ >>> numeric_part("16b")
+ 16
+ """
+
+ m = re_numeric_part.match(s)
+ if m:
+ return int(m.group(1))
+ return None
+
+
+class Connection(_mysql.connection):
+ """MySQL Database Connection Object"""
+
+ default_cursor = cursors.Cursor
+
+ def __init__(self, *args, **kwargs):
+ """
+ Create a connection to the database. It is strongly recommended
+ that you only use keyword parameters. Consult the MySQL C API
+ documentation for more information.
+
+ :param str host: host to connect
+ :param str user: user to connect as
+ :param str password: password to use
+ :param str passwd: alias of password (deprecated)
+ :param str database: database to use
+ :param str db: alias of database (deprecated)
+ :param int port: TCP/IP port to connect to
+ :param str unix_socket: location of unix_socket to use
+ :param dict conv: conversion dictionary, see MySQLdb.converters
+ :param int connect_timeout:
+ number of seconds to wait before the connection attempt fails.
+
+ :param bool compress: if set, compression is enabled
+ :param str named_pipe: if set, a named pipe is used to connect (Windows only)
+ :param str init_command:
+ command which is run once the connection is created
+
+ :param str read_default_file:
+ file from which default client values are read
+
+ :param str read_default_group:
+ configuration group to use from the default file
+
+ :param type cursorclass:
+ class object, used to create cursors (keyword only)
+
+ :param bool use_unicode:
+ If True, text-like columns are returned as unicode objects
+ using the connection's character set. Otherwise, text-like
+ columns are returned as bytes. Unicode objects will always
+ be encoded to the connection's character set regardless of
+ this setting.
+ Default to True.
+
+ :param str charset:
+ If supplied, the connection character set will be changed
+ to this character set.
+
+ :param str auth_plugin:
+ If supplied, the connection default authentication plugin will be
+ changed to this value. Example values:
+ `mysql_native_password` or `caching_sha2_password`
+
+ :param str sql_mode:
+ If supplied, the session SQL mode will be changed to this
+ setting.
+ For more details and legal values, see the MySQL documentation.
+
+ :param int client_flag:
+ flags to use or 0 (see MySQL docs or constants/CLIENTS.py)
+
+ :param bool multi_statements:
+ If True, enable multi statements for clients >= 4.1.
+ Defaults to True.
+
+ :param str ssl_mode:
+ specify the security settings for connection to the server;
+ see the MySQL documentation for more details
+ (mysql_option(), MYSQL_OPT_SSL_MODE).
+ Only one of 'DISABLED', 'PREFERRED', 'REQUIRED',
+ 'VERIFY_CA', 'VERIFY_IDENTITY' can be specified.
+
+ :param dict ssl:
+ dictionary or mapping contains SSL connection parameters;
+ see the MySQL documentation for more details
+ (mysql_ssl_set()). If this is set, and the client does not
+ support SSL, NotSupportedError will be raised.
+
+ :param bool local_infile:
+ enables LOAD LOCAL INFILE; zero disables
+
+ :param bool autocommit:
+ If False (default), autocommit is disabled.
+ If True, autocommit is enabled.
+ If None, autocommit isn't set and server default is used.
+
+ :param bool binary_prefix:
+ If set, the '_binary' prefix will be used for raw byte query
+ arguments (e.g. Binary). This is disabled by default.
+
+ There are a number of undocumented, non-standard methods. See the
+ documentation for the MySQL C API for some hints on what they do.
+ """
+ from MySQLdb.constants import CLIENT, FIELD_TYPE
+ from MySQLdb.converters import conversions, _bytes_or_str
+ from weakref import proxy
+
+ kwargs2 = kwargs.copy()
+
+ if "db" in kwargs2:
+ kwargs2["database"] = kwargs2.pop("db")
+ if "passwd" in kwargs2:
+ kwargs2["password"] = kwargs2.pop("passwd")
+
+ if "conv" in kwargs:
+ conv = kwargs["conv"]
+ else:
+ conv = conversions
+
+ conv2 = {}
+ for k, v in conv.items():
+ if isinstance(k, int) and isinstance(v, list):
+ conv2[k] = v[:]
+ else:
+ conv2[k] = v
+ kwargs2["conv"] = conv2
+
+ cursorclass = kwargs2.pop("cursorclass", self.default_cursor)
+ charset = kwargs2.get("charset", "")
+ use_unicode = kwargs2.pop("use_unicode", True)
+ sql_mode = kwargs2.pop("sql_mode", "")
+ self._binary_prefix = kwargs2.pop("binary_prefix", False)
+
+ client_flag = kwargs.get("client_flag", 0)
+ client_flag |= CLIENT.MULTI_RESULTS
+ multi_statements = kwargs2.pop("multi_statements", True)
+ if multi_statements:
+ client_flag |= CLIENT.MULTI_STATEMENTS
+ kwargs2["client_flag"] = client_flag
+
+ # PEP-249 requires autocommit to be initially off
+ autocommit = kwargs2.pop("autocommit", False)
+
+ super().__init__(*args, **kwargs2)
+ self.cursorclass = cursorclass
+ self.encoders = {k: v for k, v in conv.items() if type(k) is not int}
+
+ self._server_version = tuple(
+ [numeric_part(n) for n in self.get_server_info().split(".")[:2]]
+ )
+
+ self.encoding = "ascii" # overridden in set_character_set()
+
+ if not charset:
+ charset = self.character_set_name()
+ self.set_character_set(charset)
+
+ if sql_mode:
+ self.set_sql_mode(sql_mode)
+
+ if use_unicode:
+ for t in (
+ FIELD_TYPE.STRING,
+ FIELD_TYPE.VAR_STRING,
+ FIELD_TYPE.VARCHAR,
+ FIELD_TYPE.TINY_BLOB,
+ FIELD_TYPE.MEDIUM_BLOB,
+ FIELD_TYPE.LONG_BLOB,
+ FIELD_TYPE.BLOB,
+ ):
+ self.converter[t] = _bytes_or_str
+ # Unlike other string/blob types, JSON is always text.
+ # MySQL may return JSON with charset==binary.
+ self.converter[FIELD_TYPE.JSON] = str
+
+ db = proxy(self)
+
+ def unicode_literal(u, dummy=None):
+ return db.string_literal(u.encode(db.encoding))
+
+ self.encoders[str] = unicode_literal
+
+ self._transactional = self.server_capabilities & CLIENT.TRANSACTIONS
+ if self._transactional:
+ if autocommit is not None:
+ self.autocommit(autocommit)
+ self.messages = []
+
+ def __enter__(self):
+ return self
+
+ def __exit__(self, exc_type, exc_value, traceback):
+ self.close()
+
+ def autocommit(self, on):
+ on = bool(on)
+ if self.get_autocommit() != on:
+ _mysql.connection.autocommit(self, on)
+
+ def cursor(self, cursorclass=None):
+ """
+ Create a cursor on which queries may be performed. The
+ optional cursorclass parameter is used to create the
+ Cursor. By default, self.cursorclass=cursors.Cursor is
+ used.
+ """
+ return (cursorclass or self.cursorclass)(self)
+
+ def query(self, query):
+ # Since _mysql releases GIL while querying, we need immutable buffer.
+ if isinstance(query, bytearray):
+ query = bytes(query)
+ _mysql.connection.query(self, query)
+
+ def _bytes_literal(self, bs):
+ assert isinstance(bs, (bytes, bytearray))
+ x = self.string_literal(bs) # x is escaped and quoted bytes
+ if self._binary_prefix:
+ return b"_binary" + x
+ return x
+
+ def _tuple_literal(self, t):
+ return b"(%s)" % (b",".join(map(self.literal, t)))
+
+ def literal(self, o):
+ """If o is a single object, returns an SQL literal as a string.
+ If o is a non-string sequence, the items of the sequence are
+ converted and returned as a sequence.
+
+ Non-standard. For internal use; do not use this in your
+ applications.
+ """
+ if isinstance(o, str):
+ s = self.string_literal(o.encode(self.encoding))
+ elif isinstance(o, bytearray):
+ s = self._bytes_literal(o)
+ elif isinstance(o, bytes):
+ s = self._bytes_literal(o)
+ elif isinstance(o, (tuple, list)):
+ s = self._tuple_literal(o)
+ else:
+ s = self.escape(o, self.encoders)
+ if isinstance(s, str):
+ s = s.encode(self.encoding)
+ assert isinstance(s, bytes)
+ return s
+
+ def begin(self):
+ """Explicitly begin a connection.
+
+ This method is not used when autocommit=False (default).
+ """
+ self.query(b"BEGIN")
+
+ def set_character_set(self, charset):
+ """Set the connection character set to charset."""
+ super().set_character_set(charset)
+ self.encoding = _charset_to_encoding.get(charset, charset)
+
+ def set_sql_mode(self, sql_mode):
+ """Set the connection sql_mode. See MySQL documentation for
+ legal values."""
+ if self._server_version < (4, 1):
+ raise NotSupportedError("server is too old to set sql_mode")
+ self.query("SET SESSION sql_mode='%s'" % sql_mode)
+ self.store_result()
+
+ def show_warnings(self):
+ """Return detailed information about warnings as a
+ sequence of tuples of (Level, Code, Message). This
+ is only supported in MySQL-4.1 and up. If your server
+ is an earlier version, an empty sequence is returned."""
+ if self._server_version < (4, 1):
+ return ()
+ self.query("SHOW WARNINGS")
+ r = self.store_result()
+ warnings = r.fetch_row(0)
+ return warnings
+
+ Warning = Warning
+ Error = Error
+ InterfaceError = InterfaceError
+ DatabaseError = DatabaseError
+ DataError = DataError
+ OperationalError = OperationalError
+ IntegrityError = IntegrityError
+ InternalError = InternalError
+ ProgrammingError = ProgrammingError
+ NotSupportedError = NotSupportedError
+
+
+# vim: colorcolumn=100
diff --git a/venv/Lib/site-packages/MySQLdb/constants/CLIENT.py b/venv/Lib/site-packages/MySQLdb/constants/CLIENT.py
new file mode 100644
index 0000000..35f578c
--- /dev/null
+++ b/venv/Lib/site-packages/MySQLdb/constants/CLIENT.py
@@ -0,0 +1,27 @@
+"""MySQL CLIENT constants
+
+These constants are used when creating the connection. Use bitwise-OR
+(|) to combine options together, and pass them as the client_flags
+parameter to MySQLdb.Connection. For more information on these flags,
+see the MySQL C API documentation for mysql_real_connect().
+
+"""
+
+LONG_PASSWORD = 1
+FOUND_ROWS = 2
+LONG_FLAG = 4
+CONNECT_WITH_DB = 8
+NO_SCHEMA = 16
+COMPRESS = 32
+ODBC = 64
+LOCAL_FILES = 128
+IGNORE_SPACE = 256
+CHANGE_USER = 512
+INTERACTIVE = 1024
+SSL = 2048
+IGNORE_SIGPIPE = 4096
+TRANSACTIONS = 8192 # mysql_com.h was WRONG prior to 3.23.35
+RESERVED = 16384
+SECURE_CONNECTION = 32768
+MULTI_STATEMENTS = 65536
+MULTI_RESULTS = 131072
diff --git a/venv/Lib/site-packages/MySQLdb/constants/CR.py b/venv/Lib/site-packages/MySQLdb/constants/CR.py
new file mode 100644
index 0000000..9d33cf6
--- /dev/null
+++ b/venv/Lib/site-packages/MySQLdb/constants/CR.py
@@ -0,0 +1,105 @@
+"""MySQL Connection Errors
+
+Nearly all of these raise OperationalError. COMMANDS_OUT_OF_SYNC
+raises ProgrammingError.
+
+"""
+
+if __name__ == "__main__":
+ """
+ Usage: python CR.py [/path/to/mysql/errmsg.h ...] >> CR.py
+ """
+ import fileinput
+ import re
+
+ data = {}
+ error_last = None
+ for line in fileinput.input():
+ line = re.sub(r"/\*.*?\*/", "", line)
+ m = re.match(r"^\s*#define\s+CR_([A-Z0-9_]+)\s+(\d+)(\s.*|$)", line)
+ if m:
+ name = m.group(1)
+ value = int(m.group(2))
+ if name == "ERROR_LAST":
+ if error_last is None or error_last < value:
+ error_last = value
+ continue
+ if value not in data:
+ data[value] = set()
+ data[value].add(name)
+ for value, names in sorted(data.items()):
+ for name in sorted(names):
+ print("{} = {}".format(name, value))
+ if error_last is not None:
+ print("ERROR_LAST = %s" % error_last)
+
+
+ERROR_FIRST = 2000
+MIN_ERROR = 2000
+UNKNOWN_ERROR = 2000
+SOCKET_CREATE_ERROR = 2001
+CONNECTION_ERROR = 2002
+CONN_HOST_ERROR = 2003
+IPSOCK_ERROR = 2004
+UNKNOWN_HOST = 2005
+SERVER_GONE_ERROR = 2006
+VERSION_ERROR = 2007
+OUT_OF_MEMORY = 2008
+WRONG_HOST_INFO = 2009
+LOCALHOST_CONNECTION = 2010
+TCP_CONNECTION = 2011
+SERVER_HANDSHAKE_ERR = 2012
+SERVER_LOST = 2013
+COMMANDS_OUT_OF_SYNC = 2014
+NAMEDPIPE_CONNECTION = 2015
+NAMEDPIPEWAIT_ERROR = 2016
+NAMEDPIPEOPEN_ERROR = 2017
+NAMEDPIPESETSTATE_ERROR = 2018
+CANT_READ_CHARSET = 2019
+NET_PACKET_TOO_LARGE = 2020
+EMBEDDED_CONNECTION = 2021
+PROBE_SLAVE_STATUS = 2022
+PROBE_SLAVE_HOSTS = 2023
+PROBE_SLAVE_CONNECT = 2024
+PROBE_MASTER_CONNECT = 2025
+SSL_CONNECTION_ERROR = 2026
+MALFORMED_PACKET = 2027
+WRONG_LICENSE = 2028
+NULL_POINTER = 2029
+NO_PREPARE_STMT = 2030
+PARAMS_NOT_BOUND = 2031
+DATA_TRUNCATED = 2032
+NO_PARAMETERS_EXISTS = 2033
+INVALID_PARAMETER_NO = 2034
+INVALID_BUFFER_USE = 2035
+UNSUPPORTED_PARAM_TYPE = 2036
+SHARED_MEMORY_CONNECTION = 2037
+SHARED_MEMORY_CONNECT_REQUEST_ERROR = 2038
+SHARED_MEMORY_CONNECT_ANSWER_ERROR = 2039
+SHARED_MEMORY_CONNECT_FILE_MAP_ERROR = 2040
+SHARED_MEMORY_CONNECT_MAP_ERROR = 2041
+SHARED_MEMORY_FILE_MAP_ERROR = 2042
+SHARED_MEMORY_MAP_ERROR = 2043
+SHARED_MEMORY_EVENT_ERROR = 2044
+SHARED_MEMORY_CONNECT_ABANDONED_ERROR = 2045
+SHARED_MEMORY_CONNECT_SET_ERROR = 2046
+CONN_UNKNOW_PROTOCOL = 2047
+INVALID_CONN_HANDLE = 2048
+UNUSED_1 = 2049
+FETCH_CANCELED = 2050
+NO_DATA = 2051
+NO_STMT_METADATA = 2052
+NO_RESULT_SET = 2053
+NOT_IMPLEMENTED = 2054
+SERVER_LOST_EXTENDED = 2055
+STMT_CLOSED = 2056
+NEW_STMT_METADATA = 2057
+ALREADY_CONNECTED = 2058
+AUTH_PLUGIN_CANNOT_LOAD = 2059
+DUPLICATE_CONNECTION_ATTR = 2060
+AUTH_PLUGIN_ERR = 2061
+INSECURE_API_ERR = 2062
+FILE_NAME_TOO_LONG = 2063
+SSL_FIPS_MODE_ERR = 2064
+MAX_ERROR = 2999
+ERROR_LAST = 2064
diff --git a/venv/Lib/site-packages/MySQLdb/constants/ER.py b/venv/Lib/site-packages/MySQLdb/constants/ER.py
new file mode 100644
index 0000000..fcd5bf2
--- /dev/null
+++ b/venv/Lib/site-packages/MySQLdb/constants/ER.py
@@ -0,0 +1,827 @@
+"""MySQL ER Constants
+
+These constants are error codes for the bulk of the error conditions
+that may occur.
+"""
+
+if __name__ == "__main__":
+ """
+ Usage: python ER.py [/path/to/mysql/mysqld_error.h ...] >> ER.py
+ """
+ import fileinput
+ import re
+
+ data = {}
+ error_last = None
+ for line in fileinput.input():
+ line = re.sub(r"/\*.*?\*/", "", line)
+ m = re.match(r"^\s*#define\s+((ER|WARN)_[A-Z0-9_]+)\s+(\d+)\s*", line)
+ if m:
+ name = m.group(1)
+ if name.startswith("ER_"):
+ name = name[3:]
+ value = int(m.group(3))
+ if name == "ERROR_LAST":
+ if error_last is None or error_last < value:
+ error_last = value
+ continue
+ if value not in data:
+ data[value] = set()
+ data[value].add(name)
+ for value, names in sorted(data.items()):
+ for name in sorted(names):
+ print("{} = {}".format(name, value))
+ if error_last is not None:
+ print("ERROR_LAST = %s" % error_last)
+
+
+ERROR_FIRST = 1000
+NO = 1002
+YES = 1003
+CANT_CREATE_FILE = 1004
+CANT_CREATE_TABLE = 1005
+CANT_CREATE_DB = 1006
+DB_CREATE_EXISTS = 1007
+DB_DROP_EXISTS = 1008
+DB_DROP_RMDIR = 1010
+CANT_FIND_SYSTEM_REC = 1012
+CANT_GET_STAT = 1013
+CANT_LOCK = 1015
+CANT_OPEN_FILE = 1016
+FILE_NOT_FOUND = 1017
+CANT_READ_DIR = 1018
+CHECKREAD = 1020
+DUP_KEY = 1022
+ERROR_ON_READ = 1024
+ERROR_ON_RENAME = 1025
+ERROR_ON_WRITE = 1026
+FILE_USED = 1027
+FILSORT_ABORT = 1028
+GET_ERRNO = 1030
+ILLEGAL_HA = 1031
+KEY_NOT_FOUND = 1032
+NOT_FORM_FILE = 1033
+NOT_KEYFILE = 1034
+OLD_KEYFILE = 1035
+OPEN_AS_READONLY = 1036
+OUTOFMEMORY = 1037
+OUT_OF_SORTMEMORY = 1038
+CON_COUNT_ERROR = 1040
+OUT_OF_RESOURCES = 1041
+BAD_HOST_ERROR = 1042
+HANDSHAKE_ERROR = 1043
+DBACCESS_DENIED_ERROR = 1044
+ACCESS_DENIED_ERROR = 1045
+NO_DB_ERROR = 1046
+UNKNOWN_COM_ERROR = 1047
+BAD_NULL_ERROR = 1048
+BAD_DB_ERROR = 1049
+TABLE_EXISTS_ERROR = 1050
+BAD_TABLE_ERROR = 1051
+NON_UNIQ_ERROR = 1052
+SERVER_SHUTDOWN = 1053
+BAD_FIELD_ERROR = 1054
+WRONG_FIELD_WITH_GROUP = 1055
+WRONG_GROUP_FIELD = 1056
+WRONG_SUM_SELECT = 1057
+WRONG_VALUE_COUNT = 1058
+TOO_LONG_IDENT = 1059
+DUP_FIELDNAME = 1060
+DUP_KEYNAME = 1061
+DUP_ENTRY = 1062
+WRONG_FIELD_SPEC = 1063
+PARSE_ERROR = 1064
+EMPTY_QUERY = 1065
+NONUNIQ_TABLE = 1066
+INVALID_DEFAULT = 1067
+MULTIPLE_PRI_KEY = 1068
+TOO_MANY_KEYS = 1069
+TOO_MANY_KEY_PARTS = 1070
+TOO_LONG_KEY = 1071
+KEY_COLUMN_DOES_NOT_EXITS = 1072
+BLOB_USED_AS_KEY = 1073
+TOO_BIG_FIELDLENGTH = 1074
+WRONG_AUTO_KEY = 1075
+READY = 1076
+SHUTDOWN_COMPLETE = 1079
+FORCING_CLOSE = 1080
+IPSOCK_ERROR = 1081
+NO_SUCH_INDEX = 1082
+WRONG_FIELD_TERMINATORS = 1083
+BLOBS_AND_NO_TERMINATED = 1084
+TEXTFILE_NOT_READABLE = 1085
+FILE_EXISTS_ERROR = 1086
+LOAD_INFO = 1087
+ALTER_INFO = 1088
+WRONG_SUB_KEY = 1089
+CANT_REMOVE_ALL_FIELDS = 1090
+CANT_DROP_FIELD_OR_KEY = 1091
+INSERT_INFO = 1092
+UPDATE_TABLE_USED = 1093
+NO_SUCH_THREAD = 1094
+KILL_DENIED_ERROR = 1095
+NO_TABLES_USED = 1096
+TOO_BIG_SET = 1097
+NO_UNIQUE_LOGFILE = 1098
+TABLE_NOT_LOCKED_FOR_WRITE = 1099
+TABLE_NOT_LOCKED = 1100
+BLOB_CANT_HAVE_DEFAULT = 1101
+WRONG_DB_NAME = 1102
+WRONG_TABLE_NAME = 1103
+TOO_BIG_SELECT = 1104
+UNKNOWN_ERROR = 1105
+UNKNOWN_PROCEDURE = 1106
+WRONG_PARAMCOUNT_TO_PROCEDURE = 1107
+WRONG_PARAMETERS_TO_PROCEDURE = 1108
+UNKNOWN_TABLE = 1109
+FIELD_SPECIFIED_TWICE = 1110
+INVALID_GROUP_FUNC_USE = 1111
+UNSUPPORTED_EXTENSION = 1112
+TABLE_MUST_HAVE_COLUMNS = 1113
+RECORD_FILE_FULL = 1114
+UNKNOWN_CHARACTER_SET = 1115
+TOO_MANY_TABLES = 1116
+TOO_MANY_FIELDS = 1117
+TOO_BIG_ROWSIZE = 1118
+STACK_OVERRUN = 1119
+WRONG_OUTER_JOIN_UNUSED = 1120
+NULL_COLUMN_IN_INDEX = 1121
+CANT_FIND_UDF = 1122
+CANT_INITIALIZE_UDF = 1123
+UDF_NO_PATHS = 1124
+UDF_EXISTS = 1125
+CANT_OPEN_LIBRARY = 1126
+CANT_FIND_DL_ENTRY = 1127
+FUNCTION_NOT_DEFINED = 1128
+HOST_IS_BLOCKED = 1129
+HOST_NOT_PRIVILEGED = 1130
+PASSWORD_ANONYMOUS_USER = 1131
+PASSWORD_NOT_ALLOWED = 1132
+PASSWORD_NO_MATCH = 1133
+UPDATE_INFO = 1134
+CANT_CREATE_THREAD = 1135
+WRONG_VALUE_COUNT_ON_ROW = 1136
+CANT_REOPEN_TABLE = 1137
+INVALID_USE_OF_NULL = 1138
+REGEXP_ERROR = 1139
+MIX_OF_GROUP_FUNC_AND_FIELDS = 1140
+NONEXISTING_GRANT = 1141
+TABLEACCESS_DENIED_ERROR = 1142
+COLUMNACCESS_DENIED_ERROR = 1143
+ILLEGAL_GRANT_FOR_TABLE = 1144
+GRANT_WRONG_HOST_OR_USER = 1145
+NO_SUCH_TABLE = 1146
+NONEXISTING_TABLE_GRANT = 1147
+NOT_ALLOWED_COMMAND = 1148
+SYNTAX_ERROR = 1149
+ABORTING_CONNECTION = 1152
+NET_PACKET_TOO_LARGE = 1153
+NET_READ_ERROR_FROM_PIPE = 1154
+NET_FCNTL_ERROR = 1155
+NET_PACKETS_OUT_OF_ORDER = 1156
+NET_UNCOMPRESS_ERROR = 1157
+NET_READ_ERROR = 1158
+NET_READ_INTERRUPTED = 1159
+NET_ERROR_ON_WRITE = 1160
+NET_WRITE_INTERRUPTED = 1161
+TOO_LONG_STRING = 1162
+TABLE_CANT_HANDLE_BLOB = 1163
+TABLE_CANT_HANDLE_AUTO_INCREMENT = 1164
+WRONG_COLUMN_NAME = 1166
+WRONG_KEY_COLUMN = 1167
+WRONG_MRG_TABLE = 1168
+DUP_UNIQUE = 1169
+BLOB_KEY_WITHOUT_LENGTH = 1170
+PRIMARY_CANT_HAVE_NULL = 1171
+TOO_MANY_ROWS = 1172
+REQUIRES_PRIMARY_KEY = 1173
+UPDATE_WITHOUT_KEY_IN_SAFE_MODE = 1175
+KEY_DOES_NOT_EXITS = 1176
+CHECK_NO_SUCH_TABLE = 1177
+CHECK_NOT_IMPLEMENTED = 1178
+CANT_DO_THIS_DURING_AN_TRANSACTION = 1179
+ERROR_DURING_COMMIT = 1180
+ERROR_DURING_ROLLBACK = 1181
+ERROR_DURING_FLUSH_LOGS = 1182
+NEW_ABORTING_CONNECTION = 1184
+MASTER = 1188
+MASTER_NET_READ = 1189
+MASTER_NET_WRITE = 1190
+FT_MATCHING_KEY_NOT_FOUND = 1191
+LOCK_OR_ACTIVE_TRANSACTION = 1192
+UNKNOWN_SYSTEM_VARIABLE = 1193
+CRASHED_ON_USAGE = 1194
+CRASHED_ON_REPAIR = 1195
+WARNING_NOT_COMPLETE_ROLLBACK = 1196
+TRANS_CACHE_FULL = 1197
+SLAVE_NOT_RUNNING = 1199
+BAD_SLAVE = 1200
+MASTER_INFO = 1201
+SLAVE_THREAD = 1202
+TOO_MANY_USER_CONNECTIONS = 1203
+SET_CONSTANTS_ONLY = 1204
+LOCK_WAIT_TIMEOUT = 1205
+LOCK_TABLE_FULL = 1206
+READ_ONLY_TRANSACTION = 1207
+WRONG_ARGUMENTS = 1210
+NO_PERMISSION_TO_CREATE_USER = 1211
+LOCK_DEADLOCK = 1213
+TABLE_CANT_HANDLE_FT = 1214
+CANNOT_ADD_FOREIGN = 1215
+NO_REFERENCED_ROW = 1216
+ROW_IS_REFERENCED = 1217
+CONNECT_TO_MASTER = 1218
+ERROR_WHEN_EXECUTING_COMMAND = 1220
+WRONG_USAGE = 1221
+WRONG_NUMBER_OF_COLUMNS_IN_SELECT = 1222
+CANT_UPDATE_WITH_READLOCK = 1223
+MIXING_NOT_ALLOWED = 1224
+DUP_ARGUMENT = 1225
+USER_LIMIT_REACHED = 1226
+SPECIFIC_ACCESS_DENIED_ERROR = 1227
+LOCAL_VARIABLE = 1228
+GLOBAL_VARIABLE = 1229
+NO_DEFAULT = 1230
+WRONG_VALUE_FOR_VAR = 1231
+WRONG_TYPE_FOR_VAR = 1232
+VAR_CANT_BE_READ = 1233
+CANT_USE_OPTION_HERE = 1234
+NOT_SUPPORTED_YET = 1235
+MASTER_FATAL_ERROR_READING_BINLOG = 1236
+SLAVE_IGNORED_TABLE = 1237
+INCORRECT_GLOBAL_LOCAL_VAR = 1238
+WRONG_FK_DEF = 1239
+KEY_REF_DO_NOT_MATCH_TABLE_REF = 1240
+OPERAND_COLUMNS = 1241
+SUBQUERY_NO_1_ROW = 1242
+UNKNOWN_STMT_HANDLER = 1243
+CORRUPT_HELP_DB = 1244
+AUTO_CONVERT = 1246
+ILLEGAL_REFERENCE = 1247
+DERIVED_MUST_HAVE_ALIAS = 1248
+SELECT_REDUCED = 1249
+TABLENAME_NOT_ALLOWED_HERE = 1250
+NOT_SUPPORTED_AUTH_MODE = 1251
+SPATIAL_CANT_HAVE_NULL = 1252
+COLLATION_CHARSET_MISMATCH = 1253
+TOO_BIG_FOR_UNCOMPRESS = 1256
+ZLIB_Z_MEM_ERROR = 1257
+ZLIB_Z_BUF_ERROR = 1258
+ZLIB_Z_DATA_ERROR = 1259
+CUT_VALUE_GROUP_CONCAT = 1260
+WARN_TOO_FEW_RECORDS = 1261
+WARN_TOO_MANY_RECORDS = 1262
+WARN_NULL_TO_NOTNULL = 1263
+WARN_DATA_OUT_OF_RANGE = 1264
+WARN_DATA_TRUNCATED = 1265
+WARN_USING_OTHER_HANDLER = 1266
+CANT_AGGREGATE_2COLLATIONS = 1267
+REVOKE_GRANTS = 1269
+CANT_AGGREGATE_3COLLATIONS = 1270
+CANT_AGGREGATE_NCOLLATIONS = 1271
+VARIABLE_IS_NOT_STRUCT = 1272
+UNKNOWN_COLLATION = 1273
+SLAVE_IGNORED_SSL_PARAMS = 1274
+SERVER_IS_IN_SECURE_AUTH_MODE = 1275
+WARN_FIELD_RESOLVED = 1276
+BAD_SLAVE_UNTIL_COND = 1277
+MISSING_SKIP_SLAVE = 1278
+UNTIL_COND_IGNORED = 1279
+WRONG_NAME_FOR_INDEX = 1280
+WRONG_NAME_FOR_CATALOG = 1281
+BAD_FT_COLUMN = 1283
+UNKNOWN_KEY_CACHE = 1284
+WARN_HOSTNAME_WONT_WORK = 1285
+UNKNOWN_STORAGE_ENGINE = 1286
+WARN_DEPRECATED_SYNTAX = 1287
+NON_UPDATABLE_TABLE = 1288
+FEATURE_DISABLED = 1289
+OPTION_PREVENTS_STATEMENT = 1290
+DUPLICATED_VALUE_IN_TYPE = 1291
+TRUNCATED_WRONG_VALUE = 1292
+INVALID_ON_UPDATE = 1294
+UNSUPPORTED_PS = 1295
+GET_ERRMSG = 1296
+GET_TEMPORARY_ERRMSG = 1297
+UNKNOWN_TIME_ZONE = 1298
+WARN_INVALID_TIMESTAMP = 1299
+INVALID_CHARACTER_STRING = 1300
+WARN_ALLOWED_PACKET_OVERFLOWED = 1301
+CONFLICTING_DECLARATIONS = 1302
+SP_NO_RECURSIVE_CREATE = 1303
+SP_ALREADY_EXISTS = 1304
+SP_DOES_NOT_EXIST = 1305
+SP_DROP_FAILED = 1306
+SP_STORE_FAILED = 1307
+SP_LILABEL_MISMATCH = 1308
+SP_LABEL_REDEFINE = 1309
+SP_LABEL_MISMATCH = 1310
+SP_UNINIT_VAR = 1311
+SP_BADSELECT = 1312
+SP_BADRETURN = 1313
+SP_BADSTATEMENT = 1314
+UPDATE_LOG_DEPRECATED_IGNORED = 1315
+UPDATE_LOG_DEPRECATED_TRANSLATED = 1316
+QUERY_INTERRUPTED = 1317
+SP_WRONG_NO_OF_ARGS = 1318
+SP_COND_MISMATCH = 1319
+SP_NORETURN = 1320
+SP_NORETURNEND = 1321
+SP_BAD_CURSOR_QUERY = 1322
+SP_BAD_CURSOR_SELECT = 1323
+SP_CURSOR_MISMATCH = 1324
+SP_CURSOR_ALREADY_OPEN = 1325
+SP_CURSOR_NOT_OPEN = 1326
+SP_UNDECLARED_VAR = 1327
+SP_WRONG_NO_OF_FETCH_ARGS = 1328
+SP_FETCH_NO_DATA = 1329
+SP_DUP_PARAM = 1330
+SP_DUP_VAR = 1331
+SP_DUP_COND = 1332
+SP_DUP_CURS = 1333
+SP_CANT_ALTER = 1334
+SP_SUBSELECT_NYI = 1335
+STMT_NOT_ALLOWED_IN_SF_OR_TRG = 1336
+SP_VARCOND_AFTER_CURSHNDLR = 1337
+SP_CURSOR_AFTER_HANDLER = 1338
+SP_CASE_NOT_FOUND = 1339
+FPARSER_TOO_BIG_FILE = 1340
+FPARSER_BAD_HEADER = 1341
+FPARSER_EOF_IN_COMMENT = 1342
+FPARSER_ERROR_IN_PARAMETER = 1343
+FPARSER_EOF_IN_UNKNOWN_PARAMETER = 1344
+VIEW_NO_EXPLAIN = 1345
+WRONG_OBJECT = 1347
+NONUPDATEABLE_COLUMN = 1348
+VIEW_SELECT_CLAUSE = 1350
+VIEW_SELECT_VARIABLE = 1351
+VIEW_SELECT_TMPTABLE = 1352
+VIEW_WRONG_LIST = 1353
+WARN_VIEW_MERGE = 1354
+WARN_VIEW_WITHOUT_KEY = 1355
+VIEW_INVALID = 1356
+SP_NO_DROP_SP = 1357
+TRG_ALREADY_EXISTS = 1359
+TRG_DOES_NOT_EXIST = 1360
+TRG_ON_VIEW_OR_TEMP_TABLE = 1361
+TRG_CANT_CHANGE_ROW = 1362
+TRG_NO_SUCH_ROW_IN_TRG = 1363
+NO_DEFAULT_FOR_FIELD = 1364
+DIVISION_BY_ZERO = 1365
+TRUNCATED_WRONG_VALUE_FOR_FIELD = 1366
+ILLEGAL_VALUE_FOR_TYPE = 1367
+VIEW_NONUPD_CHECK = 1368
+VIEW_CHECK_FAILED = 1369
+PROCACCESS_DENIED_ERROR = 1370
+RELAY_LOG_FAIL = 1371
+UNKNOWN_TARGET_BINLOG = 1373
+IO_ERR_LOG_INDEX_READ = 1374
+BINLOG_PURGE_PROHIBITED = 1375
+FSEEK_FAIL = 1376
+BINLOG_PURGE_FATAL_ERR = 1377
+LOG_IN_USE = 1378
+LOG_PURGE_UNKNOWN_ERR = 1379
+RELAY_LOG_INIT = 1380
+NO_BINARY_LOGGING = 1381
+RESERVED_SYNTAX = 1382
+PS_MANY_PARAM = 1390
+KEY_PART_0 = 1391
+VIEW_CHECKSUM = 1392
+VIEW_MULTIUPDATE = 1393
+VIEW_NO_INSERT_FIELD_LIST = 1394
+VIEW_DELETE_MERGE_VIEW = 1395
+CANNOT_USER = 1396
+XAER_NOTA = 1397
+XAER_INVAL = 1398
+XAER_RMFAIL = 1399
+XAER_OUTSIDE = 1400
+XAER_RMERR = 1401
+XA_RBROLLBACK = 1402
+NONEXISTING_PROC_GRANT = 1403
+PROC_AUTO_GRANT_FAIL = 1404
+PROC_AUTO_REVOKE_FAIL = 1405
+DATA_TOO_LONG = 1406
+SP_BAD_SQLSTATE = 1407
+STARTUP = 1408
+LOAD_FROM_FIXED_SIZE_ROWS_TO_VAR = 1409
+CANT_CREATE_USER_WITH_GRANT = 1410
+WRONG_VALUE_FOR_TYPE = 1411
+TABLE_DEF_CHANGED = 1412
+SP_DUP_HANDLER = 1413
+SP_NOT_VAR_ARG = 1414
+SP_NO_RETSET = 1415
+CANT_CREATE_GEOMETRY_OBJECT = 1416
+BINLOG_UNSAFE_ROUTINE = 1418
+BINLOG_CREATE_ROUTINE_NEED_SUPER = 1419
+STMT_HAS_NO_OPEN_CURSOR = 1421
+COMMIT_NOT_ALLOWED_IN_SF_OR_TRG = 1422
+NO_DEFAULT_FOR_VIEW_FIELD = 1423
+SP_NO_RECURSION = 1424
+TOO_BIG_SCALE = 1425
+TOO_BIG_PRECISION = 1426
+M_BIGGER_THAN_D = 1427
+WRONG_LOCK_OF_SYSTEM_TABLE = 1428
+CONNECT_TO_FOREIGN_DATA_SOURCE = 1429
+QUERY_ON_FOREIGN_DATA_SOURCE = 1430
+FOREIGN_DATA_SOURCE_DOESNT_EXIST = 1431
+FOREIGN_DATA_STRING_INVALID_CANT_CREATE = 1432
+FOREIGN_DATA_STRING_INVALID = 1433
+TRG_IN_WRONG_SCHEMA = 1435
+STACK_OVERRUN_NEED_MORE = 1436
+TOO_LONG_BODY = 1437
+WARN_CANT_DROP_DEFAULT_KEYCACHE = 1438
+TOO_BIG_DISPLAYWIDTH = 1439
+XAER_DUPID = 1440
+DATETIME_FUNCTION_OVERFLOW = 1441
+CANT_UPDATE_USED_TABLE_IN_SF_OR_TRG = 1442
+VIEW_PREVENT_UPDATE = 1443
+PS_NO_RECURSION = 1444
+SP_CANT_SET_AUTOCOMMIT = 1445
+VIEW_FRM_NO_USER = 1447
+VIEW_OTHER_USER = 1448
+NO_SUCH_USER = 1449
+FORBID_SCHEMA_CHANGE = 1450
+ROW_IS_REFERENCED_2 = 1451
+NO_REFERENCED_ROW_2 = 1452
+SP_BAD_VAR_SHADOW = 1453
+TRG_NO_DEFINER = 1454
+OLD_FILE_FORMAT = 1455
+SP_RECURSION_LIMIT = 1456
+SP_WRONG_NAME = 1458
+TABLE_NEEDS_UPGRADE = 1459
+SP_NO_AGGREGATE = 1460
+MAX_PREPARED_STMT_COUNT_REACHED = 1461
+VIEW_RECURSIVE = 1462
+NON_GROUPING_FIELD_USED = 1463
+TABLE_CANT_HANDLE_SPKEYS = 1464
+NO_TRIGGERS_ON_SYSTEM_SCHEMA = 1465
+REMOVED_SPACES = 1466
+AUTOINC_READ_FAILED = 1467
+USERNAME = 1468
+HOSTNAME = 1469
+WRONG_STRING_LENGTH = 1470
+NON_INSERTABLE_TABLE = 1471
+ADMIN_WRONG_MRG_TABLE = 1472
+TOO_HIGH_LEVEL_OF_NESTING_FOR_SELECT = 1473
+NAME_BECOMES_EMPTY = 1474
+AMBIGUOUS_FIELD_TERM = 1475
+FOREIGN_SERVER_EXISTS = 1476
+FOREIGN_SERVER_DOESNT_EXIST = 1477
+ILLEGAL_HA_CREATE_OPTION = 1478
+PARTITION_REQUIRES_VALUES_ERROR = 1479
+PARTITION_WRONG_VALUES_ERROR = 1480
+PARTITION_MAXVALUE_ERROR = 1481
+PARTITION_WRONG_NO_PART_ERROR = 1484
+PARTITION_WRONG_NO_SUBPART_ERROR = 1485
+WRONG_EXPR_IN_PARTITION_FUNC_ERROR = 1486
+FIELD_NOT_FOUND_PART_ERROR = 1488
+INCONSISTENT_PARTITION_INFO_ERROR = 1490
+PARTITION_FUNC_NOT_ALLOWED_ERROR = 1491
+PARTITIONS_MUST_BE_DEFINED_ERROR = 1492
+RANGE_NOT_INCREASING_ERROR = 1493
+INCONSISTENT_TYPE_OF_FUNCTIONS_ERROR = 1494
+MULTIPLE_DEF_CONST_IN_LIST_PART_ERROR = 1495
+PARTITION_ENTRY_ERROR = 1496
+MIX_HANDLER_ERROR = 1497
+PARTITION_NOT_DEFINED_ERROR = 1498
+TOO_MANY_PARTITIONS_ERROR = 1499
+SUBPARTITION_ERROR = 1500
+CANT_CREATE_HANDLER_FILE = 1501
+BLOB_FIELD_IN_PART_FUNC_ERROR = 1502
+UNIQUE_KEY_NEED_ALL_FIELDS_IN_PF = 1503
+NO_PARTS_ERROR = 1504
+PARTITION_MGMT_ON_NONPARTITIONED = 1505
+FOREIGN_KEY_ON_PARTITIONED = 1506
+DROP_PARTITION_NON_EXISTENT = 1507
+DROP_LAST_PARTITION = 1508
+COALESCE_ONLY_ON_HASH_PARTITION = 1509
+REORG_HASH_ONLY_ON_SAME_NO = 1510
+REORG_NO_PARAM_ERROR = 1511
+ONLY_ON_RANGE_LIST_PARTITION = 1512
+ADD_PARTITION_SUBPART_ERROR = 1513
+ADD_PARTITION_NO_NEW_PARTITION = 1514
+COALESCE_PARTITION_NO_PARTITION = 1515
+REORG_PARTITION_NOT_EXIST = 1516
+SAME_NAME_PARTITION = 1517
+NO_BINLOG_ERROR = 1518
+CONSECUTIVE_REORG_PARTITIONS = 1519
+REORG_OUTSIDE_RANGE = 1520
+PARTITION_FUNCTION_FAILURE = 1521
+LIMITED_PART_RANGE = 1523
+PLUGIN_IS_NOT_LOADED = 1524
+WRONG_VALUE = 1525
+NO_PARTITION_FOR_GIVEN_VALUE = 1526
+FILEGROUP_OPTION_ONLY_ONCE = 1527
+CREATE_FILEGROUP_FAILED = 1528
+DROP_FILEGROUP_FAILED = 1529
+TABLESPACE_AUTO_EXTEND_ERROR = 1530
+WRONG_SIZE_NUMBER = 1531
+SIZE_OVERFLOW_ERROR = 1532
+ALTER_FILEGROUP_FAILED = 1533
+BINLOG_ROW_LOGGING_FAILED = 1534
+EVENT_ALREADY_EXISTS = 1537
+EVENT_DOES_NOT_EXIST = 1539
+EVENT_INTERVAL_NOT_POSITIVE_OR_TOO_BIG = 1542
+EVENT_ENDS_BEFORE_STARTS = 1543
+EVENT_EXEC_TIME_IN_THE_PAST = 1544
+EVENT_SAME_NAME = 1551
+DROP_INDEX_FK = 1553
+WARN_DEPRECATED_SYNTAX_WITH_VER = 1554
+CANT_LOCK_LOG_TABLE = 1556
+FOREIGN_DUPLICATE_KEY_OLD_UNUSED = 1557
+COL_COUNT_DOESNT_MATCH_PLEASE_UPDATE = 1558
+TEMP_TABLE_PREVENTS_SWITCH_OUT_OF_RBR = 1559
+STORED_FUNCTION_PREVENTS_SWITCH_BINLOG_FORMAT = 1560
+PARTITION_NO_TEMPORARY = 1562
+PARTITION_CONST_DOMAIN_ERROR = 1563
+PARTITION_FUNCTION_IS_NOT_ALLOWED = 1564
+NULL_IN_VALUES_LESS_THAN = 1566
+WRONG_PARTITION_NAME = 1567
+CANT_CHANGE_TX_CHARACTERISTICS = 1568
+DUP_ENTRY_AUTOINCREMENT_CASE = 1569
+EVENT_SET_VAR_ERROR = 1571
+PARTITION_MERGE_ERROR = 1572
+BASE64_DECODE_ERROR = 1575
+EVENT_RECURSION_FORBIDDEN = 1576
+ONLY_INTEGERS_ALLOWED = 1578
+UNSUPORTED_LOG_ENGINE = 1579
+BAD_LOG_STATEMENT = 1580
+CANT_RENAME_LOG_TABLE = 1581
+WRONG_PARAMCOUNT_TO_NATIVE_FCT = 1582
+WRONG_PARAMETERS_TO_NATIVE_FCT = 1583
+WRONG_PARAMETERS_TO_STORED_FCT = 1584
+NATIVE_FCT_NAME_COLLISION = 1585
+DUP_ENTRY_WITH_KEY_NAME = 1586
+BINLOG_PURGE_EMFILE = 1587
+EVENT_CANNOT_CREATE_IN_THE_PAST = 1588
+EVENT_CANNOT_ALTER_IN_THE_PAST = 1589
+NO_PARTITION_FOR_GIVEN_VALUE_SILENT = 1591
+BINLOG_UNSAFE_STATEMENT = 1592
+BINLOG_FATAL_ERROR = 1593
+BINLOG_LOGGING_IMPOSSIBLE = 1598
+VIEW_NO_CREATION_CTX = 1599
+VIEW_INVALID_CREATION_CTX = 1600
+TRG_CORRUPTED_FILE = 1602
+TRG_NO_CREATION_CTX = 1603
+TRG_INVALID_CREATION_CTX = 1604
+EVENT_INVALID_CREATION_CTX = 1605
+TRG_CANT_OPEN_TABLE = 1606
+NO_FORMAT_DESCRIPTION_EVENT_BEFORE_BINLOG_STATEMENT = 1609
+SLAVE_CORRUPT_EVENT = 1610
+LOG_PURGE_NO_FILE = 1612
+XA_RBTIMEOUT = 1613
+XA_RBDEADLOCK = 1614
+NEED_REPREPARE = 1615
+WARN_NO_MASTER_INFO = 1617
+WARN_OPTION_IGNORED = 1618
+PLUGIN_DELETE_BUILTIN = 1619
+WARN_PLUGIN_BUSY = 1620
+VARIABLE_IS_READONLY = 1621
+WARN_ENGINE_TRANSACTION_ROLLBACK = 1622
+SLAVE_HEARTBEAT_VALUE_OUT_OF_RANGE = 1624
+NDB_REPLICATION_SCHEMA_ERROR = 1625
+CONFLICT_FN_PARSE_ERROR = 1626
+EXCEPTIONS_WRITE_ERROR = 1627
+TOO_LONG_TABLE_COMMENT = 1628
+TOO_LONG_FIELD_COMMENT = 1629
+FUNC_INEXISTENT_NAME_COLLISION = 1630
+DATABASE_NAME = 1631
+TABLE_NAME = 1632
+PARTITION_NAME = 1633
+SUBPARTITION_NAME = 1634
+TEMPORARY_NAME = 1635
+RENAMED_NAME = 1636
+TOO_MANY_CONCURRENT_TRXS = 1637
+WARN_NON_ASCII_SEPARATOR_NOT_IMPLEMENTED = 1638
+DEBUG_SYNC_TIMEOUT = 1639
+DEBUG_SYNC_HIT_LIMIT = 1640
+DUP_SIGNAL_SET = 1641
+SIGNAL_WARN = 1642
+SIGNAL_NOT_FOUND = 1643
+SIGNAL_EXCEPTION = 1644
+RESIGNAL_WITHOUT_ACTIVE_HANDLER = 1645
+SIGNAL_BAD_CONDITION_TYPE = 1646
+WARN_COND_ITEM_TRUNCATED = 1647
+COND_ITEM_TOO_LONG = 1648
+UNKNOWN_LOCALE = 1649
+SLAVE_IGNORE_SERVER_IDS = 1650
+SAME_NAME_PARTITION_FIELD = 1652
+PARTITION_COLUMN_LIST_ERROR = 1653
+WRONG_TYPE_COLUMN_VALUE_ERROR = 1654
+TOO_MANY_PARTITION_FUNC_FIELDS_ERROR = 1655
+MAXVALUE_IN_VALUES_IN = 1656
+TOO_MANY_VALUES_ERROR = 1657
+ROW_SINGLE_PARTITION_FIELD_ERROR = 1658
+FIELD_TYPE_NOT_ALLOWED_AS_PARTITION_FIELD = 1659
+PARTITION_FIELDS_TOO_LONG = 1660
+BINLOG_ROW_ENGINE_AND_STMT_ENGINE = 1661
+BINLOG_ROW_MODE_AND_STMT_ENGINE = 1662
+BINLOG_UNSAFE_AND_STMT_ENGINE = 1663
+BINLOG_ROW_INJECTION_AND_STMT_ENGINE = 1664
+BINLOG_STMT_MODE_AND_ROW_ENGINE = 1665
+BINLOG_ROW_INJECTION_AND_STMT_MODE = 1666
+BINLOG_MULTIPLE_ENGINES_AND_SELF_LOGGING_ENGINE = 1667
+BINLOG_UNSAFE_LIMIT = 1668
+BINLOG_UNSAFE_SYSTEM_TABLE = 1670
+BINLOG_UNSAFE_AUTOINC_COLUMNS = 1671
+BINLOG_UNSAFE_UDF = 1672
+BINLOG_UNSAFE_SYSTEM_VARIABLE = 1673
+BINLOG_UNSAFE_SYSTEM_FUNCTION = 1674
+BINLOG_UNSAFE_NONTRANS_AFTER_TRANS = 1675
+MESSAGE_AND_STATEMENT = 1676
+SLAVE_CANT_CREATE_CONVERSION = 1678
+INSIDE_TRANSACTION_PREVENTS_SWITCH_BINLOG_FORMAT = 1679
+PATH_LENGTH = 1680
+WARN_DEPRECATED_SYNTAX_NO_REPLACEMENT = 1681
+WRONG_NATIVE_TABLE_STRUCTURE = 1682
+WRONG_PERFSCHEMA_USAGE = 1683
+WARN_I_S_SKIPPED_TABLE = 1684
+INSIDE_TRANSACTION_PREVENTS_SWITCH_BINLOG_DIRECT = 1685
+STORED_FUNCTION_PREVENTS_SWITCH_BINLOG_DIRECT = 1686
+SPATIAL_MUST_HAVE_GEOM_COL = 1687
+TOO_LONG_INDEX_COMMENT = 1688
+LOCK_ABORTED = 1689
+DATA_OUT_OF_RANGE = 1690
+WRONG_SPVAR_TYPE_IN_LIMIT = 1691
+BINLOG_UNSAFE_MULTIPLE_ENGINES_AND_SELF_LOGGING_ENGINE = 1692
+BINLOG_UNSAFE_MIXED_STATEMENT = 1693
+INSIDE_TRANSACTION_PREVENTS_SWITCH_SQL_LOG_BIN = 1694
+STORED_FUNCTION_PREVENTS_SWITCH_SQL_LOG_BIN = 1695
+FAILED_READ_FROM_PAR_FILE = 1696
+VALUES_IS_NOT_INT_TYPE_ERROR = 1697
+ACCESS_DENIED_NO_PASSWORD_ERROR = 1698
+SET_PASSWORD_AUTH_PLUGIN = 1699
+TRUNCATE_ILLEGAL_FK = 1701
+PLUGIN_IS_PERMANENT = 1702
+SLAVE_HEARTBEAT_VALUE_OUT_OF_RANGE_MIN = 1703
+SLAVE_HEARTBEAT_VALUE_OUT_OF_RANGE_MAX = 1704
+STMT_CACHE_FULL = 1705
+MULTI_UPDATE_KEY_CONFLICT = 1706
+TABLE_NEEDS_REBUILD = 1707
+WARN_OPTION_BELOW_LIMIT = 1708
+INDEX_COLUMN_TOO_LONG = 1709
+ERROR_IN_TRIGGER_BODY = 1710
+ERROR_IN_UNKNOWN_TRIGGER_BODY = 1711
+INDEX_CORRUPT = 1712
+UNDO_RECORD_TOO_BIG = 1713
+BINLOG_UNSAFE_INSERT_IGNORE_SELECT = 1714
+BINLOG_UNSAFE_INSERT_SELECT_UPDATE = 1715
+BINLOG_UNSAFE_REPLACE_SELECT = 1716
+BINLOG_UNSAFE_CREATE_IGNORE_SELECT = 1717
+BINLOG_UNSAFE_CREATE_REPLACE_SELECT = 1718
+BINLOG_UNSAFE_UPDATE_IGNORE = 1719
+PLUGIN_NO_UNINSTALL = 1720
+PLUGIN_NO_INSTALL = 1721
+BINLOG_UNSAFE_WRITE_AUTOINC_SELECT = 1722
+BINLOG_UNSAFE_CREATE_SELECT_AUTOINC = 1723
+BINLOG_UNSAFE_INSERT_TWO_KEYS = 1724
+TABLE_IN_FK_CHECK = 1725
+UNSUPPORTED_ENGINE = 1726
+BINLOG_UNSAFE_AUTOINC_NOT_FIRST = 1727
+CANNOT_LOAD_FROM_TABLE_V2 = 1728
+MASTER_DELAY_VALUE_OUT_OF_RANGE = 1729
+ONLY_FD_AND_RBR_EVENTS_ALLOWED_IN_BINLOG_STATEMENT = 1730
+PARTITION_EXCHANGE_DIFFERENT_OPTION = 1731
+PARTITION_EXCHANGE_PART_TABLE = 1732
+PARTITION_EXCHANGE_TEMP_TABLE = 1733
+PARTITION_INSTEAD_OF_SUBPARTITION = 1734
+UNKNOWN_PARTITION = 1735
+TABLES_DIFFERENT_METADATA = 1736
+ROW_DOES_NOT_MATCH_PARTITION = 1737
+BINLOG_CACHE_SIZE_GREATER_THAN_MAX = 1738
+WARN_INDEX_NOT_APPLICABLE = 1739
+PARTITION_EXCHANGE_FOREIGN_KEY = 1740
+RPL_INFO_DATA_TOO_LONG = 1742
+BINLOG_STMT_CACHE_SIZE_GREATER_THAN_MAX = 1745
+CANT_UPDATE_TABLE_IN_CREATE_TABLE_SELECT = 1746
+PARTITION_CLAUSE_ON_NONPARTITIONED = 1747
+ROW_DOES_NOT_MATCH_GIVEN_PARTITION_SET = 1748
+CHANGE_RPL_INFO_REPOSITORY_FAILURE = 1750
+WARNING_NOT_COMPLETE_ROLLBACK_WITH_CREATED_TEMP_TABLE = 1751
+WARNING_NOT_COMPLETE_ROLLBACK_WITH_DROPPED_TEMP_TABLE = 1752
+MTS_FEATURE_IS_NOT_SUPPORTED = 1753
+MTS_UPDATED_DBS_GREATER_MAX = 1754
+MTS_CANT_PARALLEL = 1755
+MTS_INCONSISTENT_DATA = 1756
+FULLTEXT_NOT_SUPPORTED_WITH_PARTITIONING = 1757
+DA_INVALID_CONDITION_NUMBER = 1758
+INSECURE_PLAIN_TEXT = 1759
+INSECURE_CHANGE_MASTER = 1760
+FOREIGN_DUPLICATE_KEY_WITH_CHILD_INFO = 1761
+FOREIGN_DUPLICATE_KEY_WITHOUT_CHILD_INFO = 1762
+SQLTHREAD_WITH_SECURE_SLAVE = 1763
+TABLE_HAS_NO_FT = 1764
+VARIABLE_NOT_SETTABLE_IN_SF_OR_TRIGGER = 1765
+VARIABLE_NOT_SETTABLE_IN_TRANSACTION = 1766
+SET_STATEMENT_CANNOT_INVOKE_FUNCTION = 1769
+GTID_NEXT_CANT_BE_AUTOMATIC_IF_GTID_NEXT_LIST_IS_NON_NULL = 1770
+MALFORMED_GTID_SET_SPECIFICATION = 1772
+MALFORMED_GTID_SET_ENCODING = 1773
+MALFORMED_GTID_SPECIFICATION = 1774
+GNO_EXHAUSTED = 1775
+BAD_SLAVE_AUTO_POSITION = 1776
+AUTO_POSITION_REQUIRES_GTID_MODE_NOT_OFF = 1777
+CANT_DO_IMPLICIT_COMMIT_IN_TRX_WHEN_GTID_NEXT_IS_SET = 1778
+GTID_MODE_ON_REQUIRES_ENFORCE_GTID_CONSISTENCY_ON = 1779
+CANT_SET_GTID_NEXT_TO_GTID_WHEN_GTID_MODE_IS_OFF = 1781
+CANT_SET_GTID_NEXT_TO_ANONYMOUS_WHEN_GTID_MODE_IS_ON = 1782
+CANT_SET_GTID_NEXT_LIST_TO_NON_NULL_WHEN_GTID_MODE_IS_OFF = 1783
+GTID_UNSAFE_NON_TRANSACTIONAL_TABLE = 1785
+GTID_UNSAFE_CREATE_SELECT = 1786
+GTID_UNSAFE_CREATE_DROP_TEMPORARY_TABLE_IN_TRANSACTION = 1787
+GTID_MODE_CAN_ONLY_CHANGE_ONE_STEP_AT_A_TIME = 1788
+MASTER_HAS_PURGED_REQUIRED_GTIDS = 1789
+CANT_SET_GTID_NEXT_WHEN_OWNING_GTID = 1790
+UNKNOWN_EXPLAIN_FORMAT = 1791
+CANT_EXECUTE_IN_READ_ONLY_TRANSACTION = 1792
+TOO_LONG_TABLE_PARTITION_COMMENT = 1793
+SLAVE_CONFIGURATION = 1794
+INNODB_FT_LIMIT = 1795
+INNODB_NO_FT_TEMP_TABLE = 1796
+INNODB_FT_WRONG_DOCID_COLUMN = 1797
+INNODB_FT_WRONG_DOCID_INDEX = 1798
+INNODB_ONLINE_LOG_TOO_BIG = 1799
+UNKNOWN_ALTER_ALGORITHM = 1800
+UNKNOWN_ALTER_LOCK = 1801
+MTS_CHANGE_MASTER_CANT_RUN_WITH_GAPS = 1802
+MTS_RECOVERY_FAILURE = 1803
+MTS_RESET_WORKERS = 1804
+COL_COUNT_DOESNT_MATCH_CORRUPTED_V2 = 1805
+SLAVE_SILENT_RETRY_TRANSACTION = 1806
+DISCARD_FK_CHECKS_RUNNING = 1807
+TABLE_SCHEMA_MISMATCH = 1808
+TABLE_IN_SYSTEM_TABLESPACE = 1809
+IO_READ_ERROR = 1810
+IO_WRITE_ERROR = 1811
+TABLESPACE_MISSING = 1812
+TABLESPACE_EXISTS = 1813
+TABLESPACE_DISCARDED = 1814
+INTERNAL_ERROR = 1815
+INNODB_IMPORT_ERROR = 1816
+INNODB_INDEX_CORRUPT = 1817
+INVALID_YEAR_COLUMN_LENGTH = 1818
+NOT_VALID_PASSWORD = 1819
+MUST_CHANGE_PASSWORD = 1820
+FK_NO_INDEX_CHILD = 1821
+FK_NO_INDEX_PARENT = 1822
+FK_FAIL_ADD_SYSTEM = 1823
+FK_CANNOT_OPEN_PARENT = 1824
+FK_INCORRECT_OPTION = 1825
+FK_DUP_NAME = 1826
+PASSWORD_FORMAT = 1827
+FK_COLUMN_CANNOT_DROP = 1828
+FK_COLUMN_CANNOT_DROP_CHILD = 1829
+FK_COLUMN_NOT_NULL = 1830
+DUP_INDEX = 1831
+FK_COLUMN_CANNOT_CHANGE = 1832
+FK_COLUMN_CANNOT_CHANGE_CHILD = 1833
+MALFORMED_PACKET = 1835
+READ_ONLY_MODE = 1836
+GTID_NEXT_TYPE_UNDEFINED_GTID = 1837
+VARIABLE_NOT_SETTABLE_IN_SP = 1838
+CANT_SET_GTID_PURGED_WHEN_GTID_EXECUTED_IS_NOT_EMPTY = 1840
+CANT_SET_GTID_PURGED_WHEN_OWNED_GTIDS_IS_NOT_EMPTY = 1841
+GTID_PURGED_WAS_CHANGED = 1842
+GTID_EXECUTED_WAS_CHANGED = 1843
+BINLOG_STMT_MODE_AND_NO_REPL_TABLES = 1844
+ALTER_OPERATION_NOT_SUPPORTED = 1845
+ALTER_OPERATION_NOT_SUPPORTED_REASON = 1846
+ALTER_OPERATION_NOT_SUPPORTED_REASON_COPY = 1847
+ALTER_OPERATION_NOT_SUPPORTED_REASON_PARTITION = 1848
+ALTER_OPERATION_NOT_SUPPORTED_REASON_FK_RENAME = 1849
+ALTER_OPERATION_NOT_SUPPORTED_REASON_COLUMN_TYPE = 1850
+ALTER_OPERATION_NOT_SUPPORTED_REASON_FK_CHECK = 1851
+ALTER_OPERATION_NOT_SUPPORTED_REASON_NOPK = 1853
+ALTER_OPERATION_NOT_SUPPORTED_REASON_AUTOINC = 1854
+ALTER_OPERATION_NOT_SUPPORTED_REASON_HIDDEN_FTS = 1855
+ALTER_OPERATION_NOT_SUPPORTED_REASON_CHANGE_FTS = 1856
+ALTER_OPERATION_NOT_SUPPORTED_REASON_FTS = 1857
+SQL_SLAVE_SKIP_COUNTER_NOT_SETTABLE_IN_GTID_MODE = 1858
+DUP_UNKNOWN_IN_INDEX = 1859
+IDENT_CAUSES_TOO_LONG_PATH = 1860
+ALTER_OPERATION_NOT_SUPPORTED_REASON_NOT_NULL = 1861
+MUST_CHANGE_PASSWORD_LOGIN = 1862
+ROW_IN_WRONG_PARTITION = 1863
+MTS_EVENT_BIGGER_PENDING_JOBS_SIZE_MAX = 1864
+BINLOG_LOGICAL_CORRUPTION = 1866
+WARN_PURGE_LOG_IN_USE = 1867
+WARN_PURGE_LOG_IS_ACTIVE = 1868
+AUTO_INCREMENT_CONFLICT = 1869
+WARN_ON_BLOCKHOLE_IN_RBR = 1870
+SLAVE_MI_INIT_REPOSITORY = 1871
+SLAVE_RLI_INIT_REPOSITORY = 1872
+ACCESS_DENIED_CHANGE_USER_ERROR = 1873
+INNODB_READ_ONLY = 1874
+STOP_SLAVE_SQL_THREAD_TIMEOUT = 1875
+STOP_SLAVE_IO_THREAD_TIMEOUT = 1876
+TABLE_CORRUPT = 1877
+TEMP_FILE_WRITE_FAILURE = 1878
+INNODB_FT_AUX_NOT_HEX_ID = 1879
+OLD_TEMPORALS_UPGRADED = 1880
+INNODB_FORCED_RECOVERY = 1881
+AES_INVALID_IV = 1882
+PLUGIN_CANNOT_BE_UNINSTALLED = 1883
+GTID_UNSAFE_BINLOG_SPLITTABLE_STATEMENT_AND_ASSIGNED_GTID = 1884
+SLAVE_HAS_MORE_GTIDS_THAN_MASTER = 1885
+MISSING_KEY = 1886
+ERROR_LAST = 1973
diff --git a/venv/Lib/site-packages/MySQLdb/constants/FIELD_TYPE.py b/venv/Lib/site-packages/MySQLdb/constants/FIELD_TYPE.py
new file mode 100644
index 0000000..3c4eca9
--- /dev/null
+++ b/venv/Lib/site-packages/MySQLdb/constants/FIELD_TYPE.py
@@ -0,0 +1,40 @@
+"""MySQL FIELD_TYPE Constants
+
+These constants represent the various column (field) types that are
+supported by MySQL.
+"""
+
+DECIMAL = 0
+TINY = 1
+SHORT = 2
+LONG = 3
+FLOAT = 4
+DOUBLE = 5
+NULL = 6
+TIMESTAMP = 7
+LONGLONG = 8
+INT24 = 9
+DATE = 10
+TIME = 11
+DATETIME = 12
+YEAR = 13
+# NEWDATE = 14 # Internal to MySQL.
+VARCHAR = 15
+BIT = 16
+# TIMESTAMP2 = 17
+# DATETIME2 = 18
+# TIME2 = 19
+JSON = 245
+NEWDECIMAL = 246
+ENUM = 247
+SET = 248
+TINY_BLOB = 249
+MEDIUM_BLOB = 250
+LONG_BLOB = 251
+BLOB = 252
+VAR_STRING = 253
+STRING = 254
+GEOMETRY = 255
+
+CHAR = TINY
+INTERVAL = ENUM
diff --git a/venv/Lib/site-packages/MySQLdb/constants/FLAG.py b/venv/Lib/site-packages/MySQLdb/constants/FLAG.py
new file mode 100644
index 0000000..00e6c7c
--- /dev/null
+++ b/venv/Lib/site-packages/MySQLdb/constants/FLAG.py
@@ -0,0 +1,23 @@
+"""MySQL FLAG Constants
+
+These flags are used along with the FIELD_TYPE to indicate various
+properties of columns in a result set.
+
+"""
+
+NOT_NULL = 1
+PRI_KEY = 2
+UNIQUE_KEY = 4
+MULTIPLE_KEY = 8
+BLOB = 16
+UNSIGNED = 32
+ZEROFILL = 64
+BINARY = 128
+ENUM = 256
+AUTO_INCREMENT = 512
+TIMESTAMP = 1024
+SET = 2048
+NUM = 32768
+PART_KEY = 16384
+GROUP = 32768
+UNIQUE = 65536
diff --git a/venv/Lib/site-packages/MySQLdb/constants/__init__.py b/venv/Lib/site-packages/MySQLdb/constants/__init__.py
new file mode 100644
index 0000000..0372265
--- /dev/null
+++ b/venv/Lib/site-packages/MySQLdb/constants/__init__.py
@@ -0,0 +1 @@
+__all__ = ["CR", "FIELD_TYPE", "CLIENT", "ER", "FLAG"]
diff --git a/venv/Lib/site-packages/MySQLdb/converters.py b/venv/Lib/site-packages/MySQLdb/converters.py
new file mode 100644
index 0000000..33f22f7
--- /dev/null
+++ b/venv/Lib/site-packages/MySQLdb/converters.py
@@ -0,0 +1,139 @@
+"""MySQLdb type conversion module
+
+This module handles all the type conversions for MySQL. If the default
+type conversions aren't what you need, you can make your own. The
+dictionary conversions maps some kind of type to a conversion function
+which returns the corresponding value:
+
+Key: FIELD_TYPE.* (from MySQLdb.constants)
+
+Conversion function:
+
+ Arguments: string
+
+ Returns: Python object
+
+Key: Python type object (from types) or class
+
+Conversion function:
+
+ Arguments: Python object of indicated type or class AND
+ conversion dictionary
+
+ Returns: SQL literal value
+
+ Notes: Most conversion functions can ignore the dictionary, but
+ it is a required parameter. It is necessary for converting
+ things like sequences and instances.
+
+Don't modify conversions if you can avoid it. Instead, make copies
+(with the copy() method), modify the copies, and then pass them to
+MySQL.connect().
+"""
+from decimal import Decimal
+
+from MySQLdb._mysql import string_literal
+from MySQLdb.constants import FIELD_TYPE, FLAG
+from MySQLdb.times import (
+ Date,
+ DateTimeType,
+ DateTime2literal,
+ DateTimeDeltaType,
+ DateTimeDelta2literal,
+ DateTime_or_None,
+ TimeDelta_or_None,
+ Date_or_None,
+)
+from MySQLdb._exceptions import ProgrammingError
+
+import array
+
+NoneType = type(None)
+
+try:
+ ArrayType = array.ArrayType
+except AttributeError:
+ ArrayType = array.array
+
+
+def Bool2Str(s, d):
+ return b"1" if s else b"0"
+
+
+def Set2Str(s, d):
+ # Only support ascii string. Not tested.
+ return string_literal(",".join(s))
+
+
+def Thing2Str(s, d):
+ """Convert something into a string via str()."""
+ return str(s)
+
+
+def Float2Str(o, d):
+ s = repr(o)
+ if s in ("inf", "nan"):
+ raise ProgrammingError("%s can not be used with MySQL" % s)
+ if "e" not in s:
+ s += "e0"
+ return s
+
+
+def None2NULL(o, d):
+ """Convert None to NULL."""
+ return b"NULL"
+
+
+def Thing2Literal(o, d):
+ """Convert something into a SQL string literal. If using
+ MySQL-3.23 or newer, string_literal() is a method of the
+ _mysql.MYSQL object, and this function will be overridden with
+ that method when the connection is created."""
+ return string_literal(o)
+
+
+def Decimal2Literal(o, d):
+ return format(o, "f")
+
+
+def array2Str(o, d):
+ return Thing2Literal(o.tostring(), d)
+
+
+# bytes or str regarding to BINARY_FLAG.
+_bytes_or_str = ((FLAG.BINARY, bytes), (None, str))
+
+conversions = {
+ int: Thing2Str,
+ float: Float2Str,
+ NoneType: None2NULL,
+ ArrayType: array2Str,
+ bool: Bool2Str,
+ Date: Thing2Literal,
+ DateTimeType: DateTime2literal,
+ DateTimeDeltaType: DateTimeDelta2literal,
+ set: Set2Str,
+ Decimal: Decimal2Literal,
+ FIELD_TYPE.TINY: int,
+ FIELD_TYPE.SHORT: int,
+ FIELD_TYPE.LONG: int,
+ FIELD_TYPE.FLOAT: float,
+ FIELD_TYPE.DOUBLE: float,
+ FIELD_TYPE.DECIMAL: Decimal,
+ FIELD_TYPE.NEWDECIMAL: Decimal,
+ FIELD_TYPE.LONGLONG: int,
+ FIELD_TYPE.INT24: int,
+ FIELD_TYPE.YEAR: int,
+ FIELD_TYPE.TIMESTAMP: DateTime_or_None,
+ FIELD_TYPE.DATETIME: DateTime_or_None,
+ FIELD_TYPE.TIME: TimeDelta_or_None,
+ FIELD_TYPE.DATE: Date_or_None,
+ FIELD_TYPE.TINY_BLOB: bytes,
+ FIELD_TYPE.MEDIUM_BLOB: bytes,
+ FIELD_TYPE.LONG_BLOB: bytes,
+ FIELD_TYPE.BLOB: bytes,
+ FIELD_TYPE.STRING: bytes,
+ FIELD_TYPE.VAR_STRING: bytes,
+ FIELD_TYPE.VARCHAR: bytes,
+ FIELD_TYPE.JSON: bytes,
+}
diff --git a/venv/Lib/site-packages/MySQLdb/cursors.py b/venv/Lib/site-packages/MySQLdb/cursors.py
new file mode 100644
index 0000000..f8a4864
--- /dev/null
+++ b/venv/Lib/site-packages/MySQLdb/cursors.py
@@ -0,0 +1,489 @@
+"""MySQLdb Cursors
+
+This module implements Cursors of various types for MySQLdb. By
+default, MySQLdb uses the Cursor class.
+"""
+import re
+
+from ._exceptions import ProgrammingError
+
+
+#: Regular expression for :meth:`Cursor.executemany`.
+#: executemany only supports simple bulk insert.
+#: You can use it to load large dataset.
+RE_INSERT_VALUES = re.compile(
+ "".join(
+ [
+ r"\s*((?:INSERT|REPLACE)\b.+\bVALUES?\s*)",
+ r"(\(\s*(?:%s|%\(.+\)s)\s*(?:,\s*(?:%s|%\(.+\)s)\s*)*\))",
+ r"(\s*(?:ON DUPLICATE.*)?);?\s*\Z",
+ ]
+ ),
+ re.IGNORECASE | re.DOTALL,
+)
+
+
+class BaseCursor:
+ """A base for Cursor classes. Useful attributes:
+
+ description
+ A tuple of DB API 7-tuples describing the columns in
+ the last executed query; see PEP-249 for details.
+
+ description_flags
+ Tuple of column flags for last query, one entry per column
+ in the result set. Values correspond to those in
+ MySQLdb.constants.FLAG. See MySQL documentation (C API)
+ for more information. Non-standard extension.
+
+ arraysize
+ default number of rows fetchmany() will fetch
+ """
+
+ #: Max statement size which :meth:`executemany` generates.
+ #:
+ #: Max size of allowed statement is max_allowed_packet - packet_header_size.
+ #: Default value of max_allowed_packet is 1048576.
+ max_stmt_length = 64 * 1024
+
+ from ._exceptions import (
+ MySQLError,
+ Warning,
+ Error,
+ InterfaceError,
+ DatabaseError,
+ DataError,
+ OperationalError,
+ IntegrityError,
+ InternalError,
+ ProgrammingError,
+ NotSupportedError,
+ )
+
+ connection = None
+
+ def __init__(self, connection):
+ self.connection = connection
+ self.description = None
+ self.description_flags = None
+ self.rowcount = -1
+ self.arraysize = 1
+ self._executed = None
+
+ self.lastrowid = None
+ self._result = None
+ self.rownumber = None
+ self._rows = None
+
+ def close(self):
+ """Close the cursor. No further queries will be possible."""
+ try:
+ if self.connection is None:
+ return
+ while self.nextset():
+ pass
+ finally:
+ self.connection = None
+ self._result = None
+
+ def __enter__(self):
+ return self
+
+ def __exit__(self, *exc_info):
+ del exc_info
+ self.close()
+
+ def _escape_args(self, args, conn):
+ encoding = conn.encoding
+ literal = conn.literal
+
+ def ensure_bytes(x):
+ if isinstance(x, str):
+ return x.encode(encoding)
+ elif isinstance(x, tuple):
+ return tuple(map(ensure_bytes, x))
+ elif isinstance(x, list):
+ return list(map(ensure_bytes, x))
+ return x
+
+ if isinstance(args, (tuple, list)):
+ ret = tuple(literal(ensure_bytes(arg)) for arg in args)
+ elif isinstance(args, dict):
+ ret = {
+ ensure_bytes(key): literal(ensure_bytes(val))
+ for (key, val) in args.items()
+ }
+ else:
+ # If it's not a dictionary let's try escaping it anyways.
+ # Worst case it will throw a Value error
+ ret = literal(ensure_bytes(args))
+
+ ensure_bytes = None # break circular reference
+ return ret
+
+ def _check_executed(self):
+ if not self._executed:
+ raise ProgrammingError("execute() first")
+
+ def nextset(self):
+ """Advance to the next result set.
+
+ Returns None if there are no more result sets.
+ """
+ if self._executed:
+ self.fetchall()
+
+ db = self._get_db()
+ nr = db.next_result()
+ if nr == -1:
+ return None
+ self._do_get_result(db)
+ self._post_get_result()
+ return 1
+
+ def _do_get_result(self, db):
+ self._result = result = self._get_result()
+ if result is None:
+ self.description = self.description_flags = None
+ else:
+ self.description = result.describe()
+ self.description_flags = result.field_flags()
+
+ self.rowcount = db.affected_rows()
+ self.rownumber = 0
+ self.lastrowid = db.insert_id()
+
+ def _post_get_result(self):
+ pass
+
+ def setinputsizes(self, *args):
+ """Does nothing, required by DB API."""
+
+ def setoutputsizes(self, *args):
+ """Does nothing, required by DB API."""
+
+ def _get_db(self):
+ con = self.connection
+ if con is None:
+ raise ProgrammingError("cursor closed")
+ return con
+
+ def execute(self, query, args=None):
+ """Execute a query.
+
+ query -- string, query to execute on server
+ args -- optional sequence or mapping, parameters to use with query.
+
+ Note: If args is a sequence, then %s must be used as the
+ parameter placeholder in the query. If a mapping is used,
+ %(key)s must be used as the placeholder.
+
+ Returns integer represents rows affected, if any
+ """
+ while self.nextset():
+ pass
+ db = self._get_db()
+
+ if isinstance(query, str):
+ query = query.encode(db.encoding)
+
+ if args is not None:
+ if isinstance(args, dict):
+ nargs = {}
+ for key, item in args.items():
+ if isinstance(key, str):
+ key = key.encode(db.encoding)
+ nargs[key] = db.literal(item)
+ args = nargs
+ else:
+ args = tuple(map(db.literal, args))
+ try:
+ query = query % args
+ except TypeError as m:
+ raise ProgrammingError(str(m))
+
+ assert isinstance(query, (bytes, bytearray))
+ res = self._query(query)
+ return res
+
+ def executemany(self, query, args):
+ # type: (str, list) -> int
+ """Execute a multi-row query.
+
+ :param query: query to execute on server
+ :param args: Sequence of sequences or mappings. It is used as parameter.
+ :return: Number of rows affected, if any.
+
+ This method improves performance on multiple-row INSERT and
+ REPLACE. Otherwise it is equivalent to looping over args with
+ execute().
+ """
+ if not args:
+ return
+
+ m = RE_INSERT_VALUES.match(query)
+ if m:
+ q_prefix = m.group(1) % ()
+ q_values = m.group(2).rstrip()
+ q_postfix = m.group(3) or ""
+ assert q_values[0] == "(" and q_values[-1] == ")"
+ return self._do_execute_many(
+ q_prefix,
+ q_values,
+ q_postfix,
+ args,
+ self.max_stmt_length,
+ self._get_db().encoding,
+ )
+
+ self.rowcount = sum(self.execute(query, arg) for arg in args)
+ return self.rowcount
+
+ def _do_execute_many(
+ self, prefix, values, postfix, args, max_stmt_length, encoding
+ ):
+ conn = self._get_db()
+ escape = self._escape_args
+ if isinstance(prefix, str):
+ prefix = prefix.encode(encoding)
+ if isinstance(values, str):
+ values = values.encode(encoding)
+ if isinstance(postfix, str):
+ postfix = postfix.encode(encoding)
+ sql = bytearray(prefix)
+ args = iter(args)
+ v = values % escape(next(args), conn)
+ sql += v
+ rows = 0
+ for arg in args:
+ v = values % escape(arg, conn)
+ if len(sql) + len(v) + len(postfix) + 1 > max_stmt_length:
+ rows += self.execute(sql + postfix)
+ sql = bytearray(prefix)
+ else:
+ sql += b","
+ sql += v
+ rows += self.execute(sql + postfix)
+ self.rowcount = rows
+ return rows
+
+ def callproc(self, procname, args=()):
+ """Execute stored procedure procname with args
+
+ procname -- string, name of procedure to execute on server
+
+ args -- Sequence of parameters to use with procedure
+
+ Returns the original args.
+
+ Compatibility warning: PEP-249 specifies that any modified
+ parameters must be returned. This is currently impossible
+ as they are only available by storing them in a server
+ variable and then retrieved by a query. Since stored
+ procedures return zero or more result sets, there is no
+ reliable way to get at OUT or INOUT parameters via callproc.
+ The server variables are named @_procname_n, where procname
+ is the parameter above and n is the position of the parameter
+ (from zero). Once all result sets generated by the procedure
+ have been fetched, you can issue a SELECT @_procname_0, ...
+ query using .execute() to get any OUT or INOUT values.
+
+ Compatibility warning: The act of calling a stored procedure
+ itself creates an empty result set. This appears after any
+ result sets generated by the procedure. This is non-standard
+ behavior with respect to the DB-API. Be sure to use nextset()
+ to advance through all result sets; otherwise you may get
+ disconnected.
+ """
+ db = self._get_db()
+ if isinstance(procname, str):
+ procname = procname.encode(db.encoding)
+ if args:
+ fmt = b"@_" + procname + b"_%d=%s"
+ q = b"SET %s" % b",".join(
+ fmt % (index, db.literal(arg)) for index, arg in enumerate(args)
+ )
+ self._query(q)
+ self.nextset()
+
+ q = b"CALL %s(%s)" % (
+ procname,
+ b",".join([b"@_%s_%d" % (procname, i) for i in range(len(args))]),
+ )
+ self._query(q)
+ return args
+
+ def _query(self, q):
+ db = self._get_db()
+ self._result = None
+ db.query(q)
+ self._do_get_result(db)
+ self._post_get_result()
+ self._executed = q
+ return self.rowcount
+
+ def _fetch_row(self, size=1):
+ if not self._result:
+ return ()
+ return self._result.fetch_row(size, self._fetch_type)
+
+ def __iter__(self):
+ return iter(self.fetchone, None)
+
+ Warning = Warning
+ Error = Error
+ InterfaceError = InterfaceError
+ DatabaseError = DatabaseError
+ DataError = DataError
+ OperationalError = OperationalError
+ IntegrityError = IntegrityError
+ InternalError = InternalError
+ ProgrammingError = ProgrammingError
+ NotSupportedError = NotSupportedError
+
+
+class CursorStoreResultMixIn:
+ """This is a MixIn class which causes the entire result set to be
+ stored on the client side, i.e. it uses mysql_store_result(). If the
+ result set can be very large, consider adding a LIMIT clause to your
+ query, or using CursorUseResultMixIn instead."""
+
+ def _get_result(self):
+ return self._get_db().store_result()
+
+ def _post_get_result(self):
+ self._rows = self._fetch_row(0)
+ self._result = None
+
+ def fetchone(self):
+ """Fetches a single row from the cursor. None indicates that
+ no more rows are available."""
+ self._check_executed()
+ if self.rownumber >= len(self._rows):
+ return None
+ result = self._rows[self.rownumber]
+ self.rownumber = self.rownumber + 1
+ return result
+
+ def fetchmany(self, size=None):
+ """Fetch up to size rows from the cursor. Result set may be smaller
+ than size. If size is not defined, cursor.arraysize is used."""
+ self._check_executed()
+ end = self.rownumber + (size or self.arraysize)
+ result = self._rows[self.rownumber : end]
+ self.rownumber = min(end, len(self._rows))
+ return result
+
+ def fetchall(self):
+ """Fetches all available rows from the cursor."""
+ self._check_executed()
+ if self.rownumber:
+ result = self._rows[self.rownumber :]
+ else:
+ result = self._rows
+ self.rownumber = len(self._rows)
+ return result
+
+ def scroll(self, value, mode="relative"):
+ """Scroll the cursor in the result set to a new position according
+ to mode.
+
+ If mode is 'relative' (default), value is taken as offset to
+ the current position in the result set, if set to 'absolute',
+ value states an absolute target position."""
+ self._check_executed()
+ if mode == "relative":
+ r = self.rownumber + value
+ elif mode == "absolute":
+ r = value
+ else:
+ raise ProgrammingError("unknown scroll mode %s" % repr(mode))
+ if r < 0 or r >= len(self._rows):
+ raise IndexError("out of range")
+ self.rownumber = r
+
+ def __iter__(self):
+ self._check_executed()
+ result = self.rownumber and self._rows[self.rownumber :] or self._rows
+ return iter(result)
+
+
+class CursorUseResultMixIn:
+
+ """This is a MixIn class which causes the result set to be stored
+ in the server and sent row-by-row to client side, i.e. it uses
+ mysql_use_result(). You MUST retrieve the entire result set and
+ close() the cursor before additional queries can be performed on
+ the connection."""
+
+ def _get_result(self):
+ return self._get_db().use_result()
+
+ def fetchone(self):
+ """Fetches a single row from the cursor."""
+ self._check_executed()
+ r = self._fetch_row(1)
+ if not r:
+ return None
+ self.rownumber = self.rownumber + 1
+ return r[0]
+
+ def fetchmany(self, size=None):
+ """Fetch up to size rows from the cursor. Result set may be smaller
+ than size. If size is not defined, cursor.arraysize is used."""
+ self._check_executed()
+ r = self._fetch_row(size or self.arraysize)
+ self.rownumber = self.rownumber + len(r)
+ return r
+
+ def fetchall(self):
+ """Fetches all available rows from the cursor."""
+ self._check_executed()
+ r = self._fetch_row(0)
+ self.rownumber = self.rownumber + len(r)
+ return r
+
+ def __iter__(self):
+ return self
+
+ def next(self):
+ row = self.fetchone()
+ if row is None:
+ raise StopIteration
+ return row
+
+ __next__ = next
+
+
+class CursorTupleRowsMixIn:
+ """This is a MixIn class that causes all rows to be returned as tuples,
+ which is the standard form required by DB API."""
+
+ _fetch_type = 0
+
+
+class CursorDictRowsMixIn:
+ """This is a MixIn class that causes all rows to be returned as
+ dictionaries. This is a non-standard feature."""
+
+ _fetch_type = 1
+
+
+class Cursor(CursorStoreResultMixIn, CursorTupleRowsMixIn, BaseCursor):
+ """This is the standard Cursor class that returns rows as tuples
+ and stores the result set in the client."""
+
+
+class DictCursor(CursorStoreResultMixIn, CursorDictRowsMixIn, BaseCursor):
+ """This is a Cursor class that returns rows as dictionaries and
+ stores the result set in the client."""
+
+
+class SSCursor(CursorUseResultMixIn, CursorTupleRowsMixIn, BaseCursor):
+ """This is a Cursor class that returns rows as tuples and stores
+ the result set in the server."""
+
+
+class SSDictCursor(CursorUseResultMixIn, CursorDictRowsMixIn, BaseCursor):
+ """This is a Cursor class that returns rows as dictionaries and
+ stores the result set in the server."""
diff --git a/venv/Lib/site-packages/MySQLdb/release.py b/venv/Lib/site-packages/MySQLdb/release.py
new file mode 100644
index 0000000..38b522c
--- /dev/null
+++ b/venv/Lib/site-packages/MySQLdb/release.py
@@ -0,0 +1,4 @@
+
+__author__ = "Inada Naoki "
+version_info = (2,1,0,'final',0)
+__version__ = "2.1.0"
diff --git a/venv/Lib/site-packages/MySQLdb/times.py b/venv/Lib/site-packages/MySQLdb/times.py
new file mode 100644
index 0000000..915d827
--- /dev/null
+++ b/venv/Lib/site-packages/MySQLdb/times.py
@@ -0,0 +1,150 @@
+"""times module
+
+This module provides some Date and Time classes for dealing with MySQL data.
+
+Use Python datetime module to handle date and time columns.
+"""
+from time import localtime
+from datetime import date, datetime, time, timedelta
+from MySQLdb._mysql import string_literal
+
+Date = date
+Time = time
+TimeDelta = timedelta
+Timestamp = datetime
+
+DateTimeDeltaType = timedelta
+DateTimeType = datetime
+
+
+def DateFromTicks(ticks):
+ """Convert UNIX ticks into a date instance."""
+ return date(*localtime(ticks)[:3])
+
+
+def TimeFromTicks(ticks):
+ """Convert UNIX ticks into a time instance."""
+ return time(*localtime(ticks)[3:6])
+
+
+def TimestampFromTicks(ticks):
+ """Convert UNIX ticks into a datetime instance."""
+ return datetime(*localtime(ticks)[:6])
+
+
+format_TIME = format_DATE = str
+
+
+def format_TIMEDELTA(v):
+ seconds = int(v.seconds) % 60
+ minutes = int(v.seconds // 60) % 60
+ hours = int(v.seconds // 3600) % 24
+ return "%d %d:%d:%d" % (v.days, hours, minutes, seconds)
+
+
+def format_TIMESTAMP(d):
+ """
+ :type d: datetime.datetime
+ """
+ if d.microsecond:
+ fmt = " ".join(
+ [
+ "{0.year:04}-{0.month:02}-{0.day:02}",
+ "{0.hour:02}:{0.minute:02}:{0.second:02}.{0.microsecond:06}",
+ ]
+ )
+ else:
+ fmt = " ".join(
+ [
+ "{0.year:04}-{0.month:02}-{0.day:02}",
+ "{0.hour:02}:{0.minute:02}:{0.second:02}",
+ ]
+ )
+ return fmt.format(d)
+
+
+def DateTime_or_None(s):
+ try:
+ if len(s) < 11:
+ return Date_or_None(s)
+
+ micros = s[20:]
+
+ if len(micros) == 0:
+ # 12:00:00
+ micros = 0
+ elif len(micros) < 7:
+ # 12:00:00.123456
+ micros = int(micros) * 10 ** (6 - len(micros))
+ else:
+ return None
+
+ return datetime(
+ int(s[:4]), # year
+ int(s[5:7]), # month
+ int(s[8:10]), # day
+ int(s[11:13] or 0), # hour
+ int(s[14:16] or 0), # minute
+ int(s[17:19] or 0), # second
+ micros, # microsecond
+ )
+ except ValueError:
+ return None
+
+
+def TimeDelta_or_None(s):
+ try:
+ h, m, s = s.split(":")
+ if "." in s:
+ s, ms = s.split(".")
+ ms = ms.ljust(6, "0")
+ else:
+ ms = 0
+ if h[0] == "-":
+ negative = True
+ else:
+ negative = False
+ h, m, s, ms = abs(int(h)), int(m), int(s), int(ms)
+ td = timedelta(hours=h, minutes=m, seconds=s, microseconds=ms)
+ if negative:
+ return -td
+ else:
+ return td
+ except ValueError:
+ # unpacking or int/float conversion failed
+ return None
+
+
+def Time_or_None(s):
+ try:
+ h, m, s = s.split(":")
+ if "." in s:
+ s, ms = s.split(".")
+ ms = ms.ljust(6, "0")
+ else:
+ ms = 0
+ h, m, s, ms = int(h), int(m), int(s), int(ms)
+ return time(hour=h, minute=m, second=s, microsecond=ms)
+ except ValueError:
+ return None
+
+
+def Date_or_None(s):
+ try:
+ return date(
+ int(s[:4]),
+ int(s[5:7]),
+ int(s[8:10]),
+ ) # year # month # day
+ except ValueError:
+ return None
+
+
+def DateTime2literal(d, c):
+ """Format a DateTime object as an ISO timestamp."""
+ return string_literal(format_TIMESTAMP(d))
+
+
+def DateTimeDelta2literal(d, c):
+ """Format a DateTimeDelta object as a time."""
+ return string_literal(format_TIMEDELTA(d))
diff --git a/venv/Lib/site-packages/PIL/BdfFontFile.py b/venv/Lib/site-packages/PIL/BdfFontFile.py
new file mode 100644
index 0000000..102b72e
--- /dev/null
+++ b/venv/Lib/site-packages/PIL/BdfFontFile.py
@@ -0,0 +1,110 @@
+#
+# The Python Imaging Library
+# $Id$
+#
+# bitmap distribution font (bdf) file parser
+#
+# history:
+# 1996-05-16 fl created (as bdf2pil)
+# 1997-08-25 fl converted to FontFile driver
+# 2001-05-25 fl removed bogus __init__ call
+# 2002-11-20 fl robustification (from Kevin Cazabon, Dmitry Vasiliev)
+# 2003-04-22 fl more robustification (from Graham Dumpleton)
+#
+# Copyright (c) 1997-2003 by Secret Labs AB.
+# Copyright (c) 1997-2003 by Fredrik Lundh.
+#
+# See the README file for information on usage and redistribution.
+#
+
+"""
+Parse X Bitmap Distribution Format (BDF)
+"""
+
+
+from . import FontFile, Image
+
+bdf_slant = {
+ "R": "Roman",
+ "I": "Italic",
+ "O": "Oblique",
+ "RI": "Reverse Italic",
+ "RO": "Reverse Oblique",
+ "OT": "Other",
+}
+
+bdf_spacing = {"P": "Proportional", "M": "Monospaced", "C": "Cell"}
+
+
+def bdf_char(f):
+ # skip to STARTCHAR
+ while True:
+ s = f.readline()
+ if not s:
+ return None
+ if s[:9] == b"STARTCHAR":
+ break
+ id = s[9:].strip().decode("ascii")
+
+ # load symbol properties
+ props = {}
+ while True:
+ s = f.readline()
+ if not s or s[:6] == b"BITMAP":
+ break
+ i = s.find(b" ")
+ props[s[:i].decode("ascii")] = s[i + 1 : -1].decode("ascii")
+
+ # load bitmap
+ bitmap = []
+ while True:
+ s = f.readline()
+ if not s or s[:7] == b"ENDCHAR":
+ break
+ bitmap.append(s[:-1])
+ bitmap = b"".join(bitmap)
+
+ [x, y, l, d] = [int(p) for p in props["BBX"].split()]
+ [dx, dy] = [int(p) for p in props["DWIDTH"].split()]
+
+ bbox = (dx, dy), (l, -d - y, x + l, -d), (0, 0, x, y)
+
+ try:
+ im = Image.frombytes("1", (x, y), bitmap, "hex", "1")
+ except ValueError:
+ # deal with zero-width characters
+ im = Image.new("1", (x, y))
+
+ return id, int(props["ENCODING"]), bbox, im
+
+
+class BdfFontFile(FontFile.FontFile):
+ """Font file plugin for the X11 BDF format."""
+
+ def __init__(self, fp):
+ super().__init__()
+
+ s = fp.readline()
+ if s[:13] != b"STARTFONT 2.1":
+ raise SyntaxError("not a valid BDF file")
+
+ props = {}
+ comments = []
+
+ while True:
+ s = fp.readline()
+ if not s or s[:13] == b"ENDPROPERTIES":
+ break
+ i = s.find(b" ")
+ props[s[:i].decode("ascii")] = s[i + 1 : -1].decode("ascii")
+ if s[:i] in [b"COMMENT", b"COPYRIGHT"]:
+ if s.find(b"LogicalFontDescription") < 0:
+ comments.append(s[i + 1 : -1].decode("ascii"))
+
+ while True:
+ c = bdf_char(fp)
+ if not c:
+ break
+ id, ch, (xy, dst, src), im = c
+ if 0 <= ch < len(self.glyph):
+ self.glyph[ch] = xy, dst, src, im
diff --git a/venv/Lib/site-packages/PIL/BlpImagePlugin.py b/venv/Lib/site-packages/PIL/BlpImagePlugin.py
new file mode 100644
index 0000000..ecd3da5
--- /dev/null
+++ b/venv/Lib/site-packages/PIL/BlpImagePlugin.py
@@ -0,0 +1,497 @@
+"""
+Blizzard Mipmap Format (.blp)
+Jerome Leclanche
+
+The contents of this file are hereby released in the public domain (CC0)
+Full text of the CC0 license:
+ https://creativecommons.org/publicdomain/zero/1.0/
+
+BLP1 files, used mostly in Warcraft III, are not fully supported.
+All types of BLP2 files used in World of Warcraft are supported.
+
+The BLP file structure consists of a header, up to 16 mipmaps of the
+texture
+
+Texture sizes must be powers of two, though the two dimensions do
+not have to be equal; 512x256 is valid, but 512x200 is not.
+The first mipmap (mipmap #0) is the full size image; each subsequent
+mipmap halves both dimensions. The final mipmap should be 1x1.
+
+BLP files come in many different flavours:
+* JPEG-compressed (type == 0) - only supported for BLP1.
+* RAW images (type == 1, encoding == 1). Each mipmap is stored as an
+ array of 8-bit values, one per pixel, left to right, top to bottom.
+ Each value is an index to the palette.
+* DXT-compressed (type == 1, encoding == 2):
+- DXT1 compression is used if alpha_encoding == 0.
+ - An additional alpha bit is used if alpha_depth == 1.
+ - DXT3 compression is used if alpha_encoding == 1.
+ - DXT5 compression is used if alpha_encoding == 7.
+"""
+
+import os
+import struct
+import warnings
+from enum import IntEnum
+from io import BytesIO
+
+from . import Image, ImageFile
+
+
+class Format(IntEnum):
+ JPEG = 0
+
+
+class Encoding(IntEnum):
+ UNCOMPRESSED = 1
+ DXT = 2
+ UNCOMPRESSED_RAW_BGRA = 3
+
+
+class AlphaEncoding(IntEnum):
+ DXT1 = 0
+ DXT3 = 1
+ DXT5 = 7
+
+
+def __getattr__(name):
+ deprecated = "deprecated and will be removed in Pillow 10 (2023-07-01). "
+ for enum, prefix in {
+ Format: "BLP_FORMAT_",
+ Encoding: "BLP_ENCODING_",
+ AlphaEncoding: "BLP_ALPHA_ENCODING_",
+ }.items():
+ if name.startswith(prefix):
+ name = name[len(prefix) :]
+ if name in enum.__members__:
+ warnings.warn(
+ prefix
+ + name
+ + " is "
+ + deprecated
+ + "Use "
+ + enum.__name__
+ + "."
+ + name
+ + " instead.",
+ DeprecationWarning,
+ stacklevel=2,
+ )
+ return enum[name]
+ raise AttributeError(f"module '{__name__}' has no attribute '{name}'")
+
+
+def unpack_565(i):
+ return (((i >> 11) & 0x1F) << 3, ((i >> 5) & 0x3F) << 2, (i & 0x1F) << 3)
+
+
+def decode_dxt1(data, alpha=False):
+ """
+ input: one "row" of data (i.e. will produce 4*width pixels)
+ """
+
+ blocks = len(data) // 8 # number of blocks in row
+ ret = (bytearray(), bytearray(), bytearray(), bytearray())
+
+ for block in range(blocks):
+ # Decode next 8-byte block.
+ idx = block * 8
+ color0, color1, bits = struct.unpack_from("> 2
+
+ a = 0xFF
+ if control == 0:
+ r, g, b = r0, g0, b0
+ elif control == 1:
+ r, g, b = r1, g1, b1
+ elif control == 2:
+ if color0 > color1:
+ r = (2 * r0 + r1) // 3
+ g = (2 * g0 + g1) // 3
+ b = (2 * b0 + b1) // 3
+ else:
+ r = (r0 + r1) // 2
+ g = (g0 + g1) // 2
+ b = (b0 + b1) // 2
+ elif control == 3:
+ if color0 > color1:
+ r = (2 * r1 + r0) // 3
+ g = (2 * g1 + g0) // 3
+ b = (2 * b1 + b0) // 3
+ else:
+ r, g, b, a = 0, 0, 0, 0
+
+ if alpha:
+ ret[j].extend([r, g, b, a])
+ else:
+ ret[j].extend([r, g, b])
+
+ return ret
+
+
+def decode_dxt3(data):
+ """
+ input: one "row" of data (i.e. will produce 4*width pixels)
+ """
+
+ blocks = len(data) // 16 # number of blocks in row
+ ret = (bytearray(), bytearray(), bytearray(), bytearray())
+
+ for block in range(blocks):
+ idx = block * 16
+ block = data[idx : idx + 16]
+ # Decode next 16-byte block.
+ bits = struct.unpack_from("<8B", block)
+ color0, color1 = struct.unpack_from(">= 4
+ else:
+ high = True
+ a &= 0xF
+ a *= 17 # We get a value between 0 and 15
+
+ color_code = (code >> 2 * (4 * j + i)) & 0x03
+
+ if color_code == 0:
+ r, g, b = r0, g0, b0
+ elif color_code == 1:
+ r, g, b = r1, g1, b1
+ elif color_code == 2:
+ r = (2 * r0 + r1) // 3
+ g = (2 * g0 + g1) // 3
+ b = (2 * b0 + b1) // 3
+ elif color_code == 3:
+ r = (2 * r1 + r0) // 3
+ g = (2 * g1 + g0) // 3
+ b = (2 * b1 + b0) // 3
+
+ ret[j].extend([r, g, b, a])
+
+ return ret
+
+
+def decode_dxt5(data):
+ """
+ input: one "row" of data (i.e. will produce 4 * width pixels)
+ """
+
+ blocks = len(data) // 16 # number of blocks in row
+ ret = (bytearray(), bytearray(), bytearray(), bytearray())
+
+ for block in range(blocks):
+ idx = block * 16
+ block = data[idx : idx + 16]
+ # Decode next 16-byte block.
+ a0, a1 = struct.unpack_from("> alphacode_index) & 0x07
+ elif alphacode_index == 15:
+ alphacode = (alphacode2 >> 15) | ((alphacode1 << 1) & 0x06)
+ else: # alphacode_index >= 18 and alphacode_index <= 45
+ alphacode = (alphacode1 >> (alphacode_index - 16)) & 0x07
+
+ if alphacode == 0:
+ a = a0
+ elif alphacode == 1:
+ a = a1
+ elif a0 > a1:
+ a = ((8 - alphacode) * a0 + (alphacode - 1) * a1) // 7
+ elif alphacode == 6:
+ a = 0
+ elif alphacode == 7:
+ a = 255
+ else:
+ a = ((6 - alphacode) * a0 + (alphacode - 1) * a1) // 5
+
+ color_code = (code >> 2 * (4 * j + i)) & 0x03
+
+ if color_code == 0:
+ r, g, b = r0, g0, b0
+ elif color_code == 1:
+ r, g, b = r1, g1, b1
+ elif color_code == 2:
+ r = (2 * r0 + r1) // 3
+ g = (2 * g0 + g1) // 3
+ b = (2 * b0 + b1) // 3
+ elif color_code == 3:
+ r = (2 * r1 + r0) // 3
+ g = (2 * g1 + g0) // 3
+ b = (2 * b1 + b0) // 3
+
+ ret[j].extend([r, g, b, a])
+
+ return ret
+
+
+class BLPFormatError(NotImplementedError):
+ pass
+
+
+def _accept(prefix):
+ return prefix[:4] in (b"BLP1", b"BLP2")
+
+
+class BlpImageFile(ImageFile.ImageFile):
+ """
+ Blizzard Mipmap Format
+ """
+
+ format = "BLP"
+ format_description = "Blizzard Mipmap Format"
+
+ def _open(self):
+ self.magic = self.fp.read(4)
+
+ self.fp.seek(5, os.SEEK_CUR)
+ (self._blp_alpha_depth,) = struct.unpack(" mode, rawmode
+ 1: ("P", "P;1"),
+ 4: ("P", "P;4"),
+ 8: ("P", "P"),
+ 16: ("RGB", "BGR;15"),
+ 24: ("RGB", "BGR"),
+ 32: ("RGB", "BGRX"),
+}
+
+
+def _accept(prefix):
+ return prefix[:2] == b"BM"
+
+
+def _dib_accept(prefix):
+ return i32(prefix) in [12, 40, 64, 108, 124]
+
+
+# =============================================================================
+# Image plugin for the Windows BMP format.
+# =============================================================================
+class BmpImageFile(ImageFile.ImageFile):
+ """Image plugin for the Windows Bitmap format (BMP)"""
+
+ # ------------------------------------------------------------- Description
+ format_description = "Windows Bitmap"
+ format = "BMP"
+
+ # -------------------------------------------------- BMP Compression values
+ COMPRESSIONS = {"RAW": 0, "RLE8": 1, "RLE4": 2, "BITFIELDS": 3, "JPEG": 4, "PNG": 5}
+ for k, v in COMPRESSIONS.items():
+ vars()[k] = v
+
+ def _bitmap(self, header=0, offset=0):
+ """Read relevant info about the BMP"""
+ read, seek = self.fp.read, self.fp.seek
+ if header:
+ seek(header)
+ file_info = {}
+ # read bmp header size @offset 14 (this is part of the header size)
+ file_info["header_size"] = i32(read(4))
+ file_info["direction"] = -1
+
+ # -------------------- If requested, read header at a specific position
+ # read the rest of the bmp header, without its size
+ header_data = ImageFile._safe_read(self.fp, file_info["header_size"] - 4)
+
+ # -------------------------------------------------- IBM OS/2 Bitmap v1
+ # ----- This format has different offsets because of width/height types
+ if file_info["header_size"] == 12:
+ file_info["width"] = i16(header_data, 0)
+ file_info["height"] = i16(header_data, 2)
+ file_info["planes"] = i16(header_data, 4)
+ file_info["bits"] = i16(header_data, 6)
+ file_info["compression"] = self.RAW
+ file_info["palette_padding"] = 3
+
+ # --------------------------------------------- Windows Bitmap v2 to v5
+ # v3, OS/2 v2, v4, v5
+ elif file_info["header_size"] in (40, 64, 108, 124):
+ file_info["y_flip"] = header_data[7] == 0xFF
+ file_info["direction"] = 1 if file_info["y_flip"] else -1
+ file_info["width"] = i32(header_data, 0)
+ file_info["height"] = (
+ i32(header_data, 4)
+ if not file_info["y_flip"]
+ else 2**32 - i32(header_data, 4)
+ )
+ file_info["planes"] = i16(header_data, 8)
+ file_info["bits"] = i16(header_data, 10)
+ file_info["compression"] = i32(header_data, 12)
+ # byte size of pixel data
+ file_info["data_size"] = i32(header_data, 16)
+ file_info["pixels_per_meter"] = (
+ i32(header_data, 20),
+ i32(header_data, 24),
+ )
+ file_info["colors"] = i32(header_data, 28)
+ file_info["palette_padding"] = 4
+ self.info["dpi"] = tuple(x / 39.3701 for x in file_info["pixels_per_meter"])
+ if file_info["compression"] == self.BITFIELDS:
+ if len(header_data) >= 52:
+ for idx, mask in enumerate(
+ ["r_mask", "g_mask", "b_mask", "a_mask"]
+ ):
+ file_info[mask] = i32(header_data, 36 + idx * 4)
+ else:
+ # 40 byte headers only have the three components in the
+ # bitfields masks, ref:
+ # https://msdn.microsoft.com/en-us/library/windows/desktop/dd183376(v=vs.85).aspx
+ # See also
+ # https://github.com/python-pillow/Pillow/issues/1293
+ # There is a 4th component in the RGBQuad, in the alpha
+ # location, but it is listed as a reserved component,
+ # and it is not generally an alpha channel
+ file_info["a_mask"] = 0x0
+ for mask in ["r_mask", "g_mask", "b_mask"]:
+ file_info[mask] = i32(read(4))
+ file_info["rgb_mask"] = (
+ file_info["r_mask"],
+ file_info["g_mask"],
+ file_info["b_mask"],
+ )
+ file_info["rgba_mask"] = (
+ file_info["r_mask"],
+ file_info["g_mask"],
+ file_info["b_mask"],
+ file_info["a_mask"],
+ )
+ else:
+ raise OSError(f"Unsupported BMP header type ({file_info['header_size']})")
+
+ # ------------------ Special case : header is reported 40, which
+ # ---------------------- is shorter than real size for bpp >= 16
+ self._size = file_info["width"], file_info["height"]
+
+ # ------- If color count was not found in the header, compute from bits
+ file_info["colors"] = (
+ file_info["colors"]
+ if file_info.get("colors", 0)
+ else (1 << file_info["bits"])
+ )
+ if offset == 14 + file_info["header_size"] and file_info["bits"] <= 8:
+ offset += 4 * file_info["colors"]
+
+ # ---------------------- Check bit depth for unusual unsupported values
+ self.mode, raw_mode = BIT2MODE.get(file_info["bits"], (None, None))
+ if self.mode is None:
+ raise OSError(f"Unsupported BMP pixel depth ({file_info['bits']})")
+
+ # ---------------- Process BMP with Bitfields compression (not palette)
+ decoder_name = "raw"
+ if file_info["compression"] == self.BITFIELDS:
+ SUPPORTED = {
+ 32: [
+ (0xFF0000, 0xFF00, 0xFF, 0x0),
+ (0xFF0000, 0xFF00, 0xFF, 0xFF000000),
+ (0xFF, 0xFF00, 0xFF0000, 0xFF000000),
+ (0x0, 0x0, 0x0, 0x0),
+ (0xFF000000, 0xFF0000, 0xFF00, 0x0),
+ ],
+ 24: [(0xFF0000, 0xFF00, 0xFF)],
+ 16: [(0xF800, 0x7E0, 0x1F), (0x7C00, 0x3E0, 0x1F)],
+ }
+ MASK_MODES = {
+ (32, (0xFF0000, 0xFF00, 0xFF, 0x0)): "BGRX",
+ (32, (0xFF000000, 0xFF0000, 0xFF00, 0x0)): "XBGR",
+ (32, (0xFF, 0xFF00, 0xFF0000, 0xFF000000)): "RGBA",
+ (32, (0xFF0000, 0xFF00, 0xFF, 0xFF000000)): "BGRA",
+ (32, (0x0, 0x0, 0x0, 0x0)): "BGRA",
+ (24, (0xFF0000, 0xFF00, 0xFF)): "BGR",
+ (16, (0xF800, 0x7E0, 0x1F)): "BGR;16",
+ (16, (0x7C00, 0x3E0, 0x1F)): "BGR;15",
+ }
+ if file_info["bits"] in SUPPORTED:
+ if (
+ file_info["bits"] == 32
+ and file_info["rgba_mask"] in SUPPORTED[file_info["bits"]]
+ ):
+ raw_mode = MASK_MODES[(file_info["bits"], file_info["rgba_mask"])]
+ self.mode = "RGBA" if "A" in raw_mode else self.mode
+ elif (
+ file_info["bits"] in (24, 16)
+ and file_info["rgb_mask"] in SUPPORTED[file_info["bits"]]
+ ):
+ raw_mode = MASK_MODES[(file_info["bits"], file_info["rgb_mask"])]
+ else:
+ raise OSError("Unsupported BMP bitfields layout")
+ else:
+ raise OSError("Unsupported BMP bitfields layout")
+ elif file_info["compression"] == self.RAW:
+ if file_info["bits"] == 32 and header == 22: # 32-bit .cur offset
+ raw_mode, self.mode = "BGRA", "RGBA"
+ elif file_info["compression"] == self.RLE8:
+ decoder_name = "bmp_rle"
+ else:
+ raise OSError(f"Unsupported BMP compression ({file_info['compression']})")
+
+ # --------------- Once the header is processed, process the palette/LUT
+ if self.mode == "P": # Paletted for 1, 4 and 8 bit images
+
+ # ---------------------------------------------------- 1-bit images
+ if not (0 < file_info["colors"] <= 65536):
+ raise OSError(f"Unsupported BMP Palette size ({file_info['colors']})")
+ else:
+ padding = file_info["palette_padding"]
+ palette = read(padding * file_info["colors"])
+ greyscale = True
+ indices = (
+ (0, 255)
+ if file_info["colors"] == 2
+ else list(range(file_info["colors"]))
+ )
+
+ # ----------------- Check if greyscale and ignore palette if so
+ for ind, val in enumerate(indices):
+ rgb = palette[ind * padding : ind * padding + 3]
+ if rgb != o8(val) * 3:
+ greyscale = False
+
+ # ------- If all colors are grey, white or black, ditch palette
+ if greyscale:
+ self.mode = "1" if file_info["colors"] == 2 else "L"
+ raw_mode = self.mode
+ else:
+ self.mode = "P"
+ self.palette = ImagePalette.raw(
+ "BGRX" if padding == 4 else "BGR", palette
+ )
+
+ # ---------------------------- Finally set the tile data for the plugin
+ self.info["compression"] = file_info["compression"]
+ self.tile = [
+ (
+ decoder_name,
+ (0, 0, file_info["width"], file_info["height"]),
+ offset or self.fp.tell(),
+ (
+ raw_mode,
+ ((file_info["width"] * file_info["bits"] + 31) >> 3) & (~3),
+ file_info["direction"],
+ ),
+ )
+ ]
+
+ def _open(self):
+ """Open file, check magic number and read header"""
+ # read 14 bytes: magic number, filesize, reserved, header final offset
+ head_data = self.fp.read(14)
+ # choke if the file does not have the required magic bytes
+ if not _accept(head_data):
+ raise SyntaxError("Not a BMP file")
+ # read the start position of the BMP image data (u32)
+ offset = i32(head_data, 10)
+ # load bitmap information (offset=raster info)
+ self._bitmap(offset=offset)
+
+
+class BmpRleDecoder(ImageFile.PyDecoder):
+ _pulls_fd = True
+
+ def decode(self, buffer):
+ data = bytearray()
+ x = 0
+ while len(data) < self.state.xsize * self.state.ysize:
+ pixels = self.fd.read(1)
+ byte = self.fd.read(1)
+ if not pixels or not byte:
+ break
+ num_pixels = pixels[0]
+ if num_pixels:
+ # encoded mode
+ if x + num_pixels > self.state.xsize:
+ # Too much data for row
+ num_pixels = max(0, self.state.xsize - x)
+ data += byte * num_pixels
+ x += num_pixels
+ else:
+ if byte[0] == 0:
+ # end of line
+ while len(data) % self.state.xsize != 0:
+ data += b"\x00"
+ x = 0
+ elif byte[0] == 1:
+ # end of bitmap
+ break
+ elif byte[0] == 2:
+ # delta
+ bytes_read = self.fd.read(2)
+ if len(bytes_read) < 2:
+ break
+ right, up = self.fd.read(2)
+ data += b"\x00" * (right + up * self.state.xsize)
+ x = len(data) % self.state.xsize
+ else:
+ # absolute mode
+ bytes_read = self.fd.read(byte[0])
+ data += bytes_read
+ if len(bytes_read) < byte[0]:
+ break
+ x += byte[0]
+
+ # align to 16-bit word boundary
+ if self.fd.tell() % 2 != 0:
+ self.fd.seek(1, os.SEEK_CUR)
+ self.set_as_raw(bytes(data), ("P", 0, self.args[-1]))
+ return -1, 0
+
+
+# =============================================================================
+# Image plugin for the DIB format (BMP alias)
+# =============================================================================
+class DibImageFile(BmpImageFile):
+
+ format = "DIB"
+ format_description = "Windows Bitmap"
+
+ def _open(self):
+ self._bitmap()
+
+
+#
+# --------------------------------------------------------------------
+# Write BMP file
+
+
+SAVE = {
+ "1": ("1", 1, 2),
+ "L": ("L", 8, 256),
+ "P": ("P", 8, 256),
+ "RGB": ("BGR", 24, 0),
+ "RGBA": ("BGRA", 32, 0),
+}
+
+
+def _dib_save(im, fp, filename):
+ _save(im, fp, filename, False)
+
+
+def _save(im, fp, filename, bitmap_header=True):
+ try:
+ rawmode, bits, colors = SAVE[im.mode]
+ except KeyError as e:
+ raise OSError(f"cannot write mode {im.mode} as BMP") from e
+
+ info = im.encoderinfo
+
+ dpi = info.get("dpi", (96, 96))
+
+ # 1 meter == 39.3701 inches
+ ppm = tuple(map(lambda x: int(x * 39.3701 + 0.5), dpi))
+
+ stride = ((im.size[0] * bits + 7) // 8 + 3) & (~3)
+ header = 40 # or 64 for OS/2 version 2
+ image = stride * im.size[1]
+
+ # bitmap header
+ if bitmap_header:
+ offset = 14 + header + colors * 4
+ file_size = offset + image
+ if file_size > 2**32 - 1:
+ raise ValueError("File size is too large for the BMP format")
+ fp.write(
+ b"BM" # file type (magic)
+ + o32(file_size) # file size
+ + o32(0) # reserved
+ + o32(offset) # image data offset
+ )
+
+ # bitmap info header
+ fp.write(
+ o32(header) # info header size
+ + o32(im.size[0]) # width
+ + o32(im.size[1]) # height
+ + o16(1) # planes
+ + o16(bits) # depth
+ + o32(0) # compression (0=uncompressed)
+ + o32(image) # size of bitmap
+ + o32(ppm[0]) # resolution
+ + o32(ppm[1]) # resolution
+ + o32(colors) # colors used
+ + o32(colors) # colors important
+ )
+
+ fp.write(b"\0" * (header - 40)) # padding (for OS/2 format)
+
+ if im.mode == "1":
+ for i in (0, 255):
+ fp.write(o8(i) * 4)
+ elif im.mode == "L":
+ for i in range(256):
+ fp.write(o8(i) * 4)
+ elif im.mode == "P":
+ fp.write(im.im.getpalette("RGB", "BGRX"))
+
+ ImageFile._save(im, fp, [("raw", (0, 0) + im.size, 0, (rawmode, stride, -1))])
+
+
+#
+# --------------------------------------------------------------------
+# Registry
+
+
+Image.register_open(BmpImageFile.format, BmpImageFile, _accept)
+Image.register_save(BmpImageFile.format, _save)
+
+Image.register_extension(BmpImageFile.format, ".bmp")
+
+Image.register_mime(BmpImageFile.format, "image/bmp")
+
+Image.register_decoder("bmp_rle", BmpRleDecoder)
+
+Image.register_open(DibImageFile.format, DibImageFile, _dib_accept)
+Image.register_save(DibImageFile.format, _dib_save)
+
+Image.register_extension(DibImageFile.format, ".dib")
+
+Image.register_mime(DibImageFile.format, "image/bmp")
diff --git a/venv/Lib/site-packages/PIL/BufrStubImagePlugin.py b/venv/Lib/site-packages/PIL/BufrStubImagePlugin.py
new file mode 100644
index 0000000..9510f73
--- /dev/null
+++ b/venv/Lib/site-packages/PIL/BufrStubImagePlugin.py
@@ -0,0 +1,73 @@
+#
+# The Python Imaging Library
+# $Id$
+#
+# BUFR stub adapter
+#
+# Copyright (c) 1996-2003 by Fredrik Lundh
+#
+# See the README file for information on usage and redistribution.
+#
+
+from . import Image, ImageFile
+
+_handler = None
+
+
+def register_handler(handler):
+ """
+ Install application-specific BUFR image handler.
+
+ :param handler: Handler object.
+ """
+ global _handler
+ _handler = handler
+
+
+# --------------------------------------------------------------------
+# Image adapter
+
+
+def _accept(prefix):
+ return prefix[:4] == b"BUFR" or prefix[:4] == b"ZCZC"
+
+
+class BufrStubImageFile(ImageFile.StubImageFile):
+
+ format = "BUFR"
+ format_description = "BUFR"
+
+ def _open(self):
+
+ offset = self.fp.tell()
+
+ if not _accept(self.fp.read(4)):
+ raise SyntaxError("Not a BUFR file")
+
+ self.fp.seek(offset)
+
+ # make something up
+ self.mode = "F"
+ self._size = 1, 1
+
+ loader = self._load()
+ if loader:
+ loader.open(self)
+
+ def _load(self):
+ return _handler
+
+
+def _save(im, fp, filename):
+ if _handler is None or not hasattr(_handler, "save"):
+ raise OSError("BUFR save handler not installed")
+ _handler.save(im, fp, filename)
+
+
+# --------------------------------------------------------------------
+# Registry
+
+Image.register_open(BufrStubImageFile.format, BufrStubImageFile, _accept)
+Image.register_save(BufrStubImageFile.format, _save)
+
+Image.register_extension(BufrStubImageFile.format, ".bufr")
diff --git a/venv/Lib/site-packages/PIL/ContainerIO.py b/venv/Lib/site-packages/PIL/ContainerIO.py
new file mode 100644
index 0000000..45e80b3
--- /dev/null
+++ b/venv/Lib/site-packages/PIL/ContainerIO.py
@@ -0,0 +1,120 @@
+#
+# The Python Imaging Library.
+# $Id$
+#
+# a class to read from a container file
+#
+# History:
+# 1995-06-18 fl Created
+# 1995-09-07 fl Added readline(), readlines()
+#
+# Copyright (c) 1997-2001 by Secret Labs AB
+# Copyright (c) 1995 by Fredrik Lundh
+#
+# See the README file for information on usage and redistribution.
+#
+
+
+import io
+
+
+class ContainerIO:
+ """
+ A file object that provides read access to a part of an existing
+ file (for example a TAR file).
+ """
+
+ def __init__(self, file, offset, length):
+ """
+ Create file object.
+
+ :param file: Existing file.
+ :param offset: Start of region, in bytes.
+ :param length: Size of region, in bytes.
+ """
+ self.fh = file
+ self.pos = 0
+ self.offset = offset
+ self.length = length
+ self.fh.seek(offset)
+
+ ##
+ # Always false.
+
+ def isatty(self):
+ return False
+
+ def seek(self, offset, mode=io.SEEK_SET):
+ """
+ Move file pointer.
+
+ :param offset: Offset in bytes.
+ :param mode: Starting position. Use 0 for beginning of region, 1
+ for current offset, and 2 for end of region. You cannot move
+ the pointer outside the defined region.
+ """
+ if mode == 1:
+ self.pos = self.pos + offset
+ elif mode == 2:
+ self.pos = self.length + offset
+ else:
+ self.pos = offset
+ # clamp
+ self.pos = max(0, min(self.pos, self.length))
+ self.fh.seek(self.offset + self.pos)
+
+ def tell(self):
+ """
+ Get current file pointer.
+
+ :returns: Offset from start of region, in bytes.
+ """
+ return self.pos
+
+ def read(self, n=0):
+ """
+ Read data.
+
+ :param n: Number of bytes to read. If omitted or zero,
+ read until end of region.
+ :returns: An 8-bit string.
+ """
+ if n:
+ n = min(n, self.length - self.pos)
+ else:
+ n = self.length - self.pos
+ if not n: # EOF
+ return b"" if "b" in self.fh.mode else ""
+ self.pos = self.pos + n
+ return self.fh.read(n)
+
+ def readline(self):
+ """
+ Read a line of text.
+
+ :returns: An 8-bit string.
+ """
+ s = b"" if "b" in self.fh.mode else ""
+ newline_character = b"\n" if "b" in self.fh.mode else "\n"
+ while True:
+ c = self.read(1)
+ if not c:
+ break
+ s = s + c
+ if c == newline_character:
+ break
+ return s
+
+ def readlines(self):
+ """
+ Read multiple lines of text.
+
+ :returns: A list of 8-bit strings.
+ """
+ lines = []
+ while True:
+ s = self.readline()
+ if not s:
+ break
+ lines.append(s)
+ return lines
diff --git a/venv/Lib/site-packages/PIL/CurImagePlugin.py b/venv/Lib/site-packages/PIL/CurImagePlugin.py
new file mode 100644
index 0000000..42af5ca
--- /dev/null
+++ b/venv/Lib/site-packages/PIL/CurImagePlugin.py
@@ -0,0 +1,75 @@
+#
+# The Python Imaging Library.
+# $Id$
+#
+# Windows Cursor support for PIL
+#
+# notes:
+# uses BmpImagePlugin.py to read the bitmap data.
+#
+# history:
+# 96-05-27 fl Created
+#
+# Copyright (c) Secret Labs AB 1997.
+# Copyright (c) Fredrik Lundh 1996.
+#
+# See the README file for information on usage and redistribution.
+#
+from . import BmpImagePlugin, Image
+from ._binary import i16le as i16
+from ._binary import i32le as i32
+
+#
+# --------------------------------------------------------------------
+
+
+def _accept(prefix):
+ return prefix[:4] == b"\0\0\2\0"
+
+
+##
+# Image plugin for Windows Cursor files.
+
+
+class CurImageFile(BmpImagePlugin.BmpImageFile):
+
+ format = "CUR"
+ format_description = "Windows Cursor"
+
+ def _open(self):
+
+ offset = self.fp.tell()
+
+ # check magic
+ s = self.fp.read(6)
+ if not _accept(s):
+ raise SyntaxError("not a CUR file")
+
+ # pick the largest cursor in the file
+ m = b""
+ for i in range(i16(s, 4)):
+ s = self.fp.read(16)
+ if not m:
+ m = s
+ elif s[0] > m[0] and s[1] > m[1]:
+ m = s
+ if not m:
+ raise TypeError("No cursors were found")
+
+ # load as bitmap
+ self._bitmap(i32(m, 12) + offset)
+
+ # patch up the bitmap height
+ self._size = self.size[0], self.size[1] // 2
+ d, e, o, a = self.tile[0]
+ self.tile[0] = d, (0, 0) + self.size, o, a
+
+ return
+
+
+#
+# --------------------------------------------------------------------
+
+Image.register_open(CurImageFile.format, CurImageFile, _accept)
+
+Image.register_extension(CurImageFile.format, ".cur")
diff --git a/venv/Lib/site-packages/PIL/DcxImagePlugin.py b/venv/Lib/site-packages/PIL/DcxImagePlugin.py
new file mode 100644
index 0000000..de21db8
--- /dev/null
+++ b/venv/Lib/site-packages/PIL/DcxImagePlugin.py
@@ -0,0 +1,89 @@
+#
+# The Python Imaging Library.
+# $Id$
+#
+# DCX file handling
+#
+# DCX is a container file format defined by Intel, commonly used
+# for fax applications. Each DCX file consists of a directory
+# (a list of file offsets) followed by a set of (usually 1-bit)
+# PCX files.
+#
+# History:
+# 1995-09-09 fl Created
+# 1996-03-20 fl Properly derived from PcxImageFile.
+# 1998-07-15 fl Renamed offset attribute to avoid name clash
+# 2002-07-30 fl Fixed file handling
+#
+# Copyright (c) 1997-98 by Secret Labs AB.
+# Copyright (c) 1995-96 by Fredrik Lundh.
+#
+# See the README file for information on usage and redistribution.
+#
+
+from . import Image
+from ._binary import i32le as i32
+from .PcxImagePlugin import PcxImageFile
+
+MAGIC = 0x3ADE68B1 # QUIZ: what's this value, then?
+
+
+def _accept(prefix):
+ return len(prefix) >= 4 and i32(prefix) == MAGIC
+
+
+##
+# Image plugin for the Intel DCX format.
+
+
+class DcxImageFile(PcxImageFile):
+
+ format = "DCX"
+ format_description = "Intel DCX"
+ _close_exclusive_fp_after_loading = False
+
+ def _open(self):
+
+ # Header
+ s = self.fp.read(4)
+ if not _accept(s):
+ raise SyntaxError("not a DCX file")
+
+ # Component directory
+ self._offset = []
+ for i in range(1024):
+ offset = i32(self.fp.read(4))
+ if not offset:
+ break
+ self._offset.append(offset)
+
+ self.__fp = self.fp
+ self.frame = None
+ self.n_frames = len(self._offset)
+ self.is_animated = self.n_frames > 1
+ self.seek(0)
+
+ def seek(self, frame):
+ if not self._seek_check(frame):
+ return
+ self.frame = frame
+ self.fp = self.__fp
+ self.fp.seek(self._offset[frame])
+ PcxImageFile._open(self)
+
+ def tell(self):
+ return self.frame
+
+ def _close__fp(self):
+ try:
+ if self.__fp != self.fp:
+ self.__fp.close()
+ except AttributeError:
+ pass
+ finally:
+ self.__fp = None
+
+
+Image.register_open(DcxImageFile.format, DcxImageFile, _accept)
+
+Image.register_extension(DcxImageFile.format, ".dcx")
diff --git a/venv/Lib/site-packages/PIL/DdsImagePlugin.py b/venv/Lib/site-packages/PIL/DdsImagePlugin.py
new file mode 100644
index 0000000..3a04bdb
--- /dev/null
+++ b/venv/Lib/site-packages/PIL/DdsImagePlugin.py
@@ -0,0 +1,249 @@
+"""
+A Pillow loader for .dds files (S3TC-compressed aka DXTC)
+Jerome Leclanche
+
+Documentation:
+ https://web.archive.org/web/20170802060935/http://oss.sgi.com/projects/ogl-sample/registry/EXT/texture_compression_s3tc.txt
+
+The contents of this file are hereby released in the public domain (CC0)
+Full text of the CC0 license:
+ https://creativecommons.org/publicdomain/zero/1.0/
+"""
+
+import struct
+from io import BytesIO
+
+from . import Image, ImageFile
+from ._binary import o32le as o32
+
+# Magic ("DDS ")
+DDS_MAGIC = 0x20534444
+
+# DDS flags
+DDSD_CAPS = 0x1
+DDSD_HEIGHT = 0x2
+DDSD_WIDTH = 0x4
+DDSD_PITCH = 0x8
+DDSD_PIXELFORMAT = 0x1000
+DDSD_MIPMAPCOUNT = 0x20000
+DDSD_LINEARSIZE = 0x80000
+DDSD_DEPTH = 0x800000
+
+# DDS caps
+DDSCAPS_COMPLEX = 0x8
+DDSCAPS_TEXTURE = 0x1000
+DDSCAPS_MIPMAP = 0x400000
+
+DDSCAPS2_CUBEMAP = 0x200
+DDSCAPS2_CUBEMAP_POSITIVEX = 0x400
+DDSCAPS2_CUBEMAP_NEGATIVEX = 0x800
+DDSCAPS2_CUBEMAP_POSITIVEY = 0x1000
+DDSCAPS2_CUBEMAP_NEGATIVEY = 0x2000
+DDSCAPS2_CUBEMAP_POSITIVEZ = 0x4000
+DDSCAPS2_CUBEMAP_NEGATIVEZ = 0x8000
+DDSCAPS2_VOLUME = 0x200000
+
+# Pixel Format
+DDPF_ALPHAPIXELS = 0x1
+DDPF_ALPHA = 0x2
+DDPF_FOURCC = 0x4
+DDPF_PALETTEINDEXED8 = 0x20
+DDPF_RGB = 0x40
+DDPF_LUMINANCE = 0x20000
+
+
+# dds.h
+
+DDS_FOURCC = DDPF_FOURCC
+DDS_RGB = DDPF_RGB
+DDS_RGBA = DDPF_RGB | DDPF_ALPHAPIXELS
+DDS_LUMINANCE = DDPF_LUMINANCE
+DDS_LUMINANCEA = DDPF_LUMINANCE | DDPF_ALPHAPIXELS
+DDS_ALPHA = DDPF_ALPHA
+DDS_PAL8 = DDPF_PALETTEINDEXED8
+
+DDS_HEADER_FLAGS_TEXTURE = DDSD_CAPS | DDSD_HEIGHT | DDSD_WIDTH | DDSD_PIXELFORMAT
+DDS_HEADER_FLAGS_MIPMAP = DDSD_MIPMAPCOUNT
+DDS_HEADER_FLAGS_VOLUME = DDSD_DEPTH
+DDS_HEADER_FLAGS_PITCH = DDSD_PITCH
+DDS_HEADER_FLAGS_LINEARSIZE = DDSD_LINEARSIZE
+
+DDS_HEIGHT = DDSD_HEIGHT
+DDS_WIDTH = DDSD_WIDTH
+
+DDS_SURFACE_FLAGS_TEXTURE = DDSCAPS_TEXTURE
+DDS_SURFACE_FLAGS_MIPMAP = DDSCAPS_COMPLEX | DDSCAPS_MIPMAP
+DDS_SURFACE_FLAGS_CUBEMAP = DDSCAPS_COMPLEX
+
+DDS_CUBEMAP_POSITIVEX = DDSCAPS2_CUBEMAP | DDSCAPS2_CUBEMAP_POSITIVEX
+DDS_CUBEMAP_NEGATIVEX = DDSCAPS2_CUBEMAP | DDSCAPS2_CUBEMAP_NEGATIVEX
+DDS_CUBEMAP_POSITIVEY = DDSCAPS2_CUBEMAP | DDSCAPS2_CUBEMAP_POSITIVEY
+DDS_CUBEMAP_NEGATIVEY = DDSCAPS2_CUBEMAP | DDSCAPS2_CUBEMAP_NEGATIVEY
+DDS_CUBEMAP_POSITIVEZ = DDSCAPS2_CUBEMAP | DDSCAPS2_CUBEMAP_POSITIVEZ
+DDS_CUBEMAP_NEGATIVEZ = DDSCAPS2_CUBEMAP | DDSCAPS2_CUBEMAP_NEGATIVEZ
+
+
+# DXT1
+DXT1_FOURCC = 0x31545844
+
+# DXT3
+DXT3_FOURCC = 0x33545844
+
+# DXT5
+DXT5_FOURCC = 0x35545844
+
+
+# dxgiformat.h
+
+DXGI_FORMAT_R8G8B8A8_TYPELESS = 27
+DXGI_FORMAT_R8G8B8A8_UNORM = 28
+DXGI_FORMAT_R8G8B8A8_UNORM_SRGB = 29
+DXGI_FORMAT_BC5_TYPELESS = 82
+DXGI_FORMAT_BC5_UNORM = 83
+DXGI_FORMAT_BC5_SNORM = 84
+DXGI_FORMAT_BC7_TYPELESS = 97
+DXGI_FORMAT_BC7_UNORM = 98
+DXGI_FORMAT_BC7_UNORM_SRGB = 99
+
+
+class DdsImageFile(ImageFile.ImageFile):
+ format = "DDS"
+ format_description = "DirectDraw Surface"
+
+ def _open(self):
+ if not _accept(self.fp.read(4)):
+ raise SyntaxError("not a DDS file")
+ (header_size,) = struct.unpack(" 0:
+ s = fp.read(min(lengthfile, 100 * 1024))
+ if not s:
+ break
+ lengthfile -= len(s)
+ f.write(s)
+
+ device = "pngalpha" if transparency else "ppmraw"
+
+ # Build Ghostscript command
+ command = [
+ "gs",
+ "-q", # quiet mode
+ "-g%dx%d" % size, # set output geometry (pixels)
+ "-r%fx%f" % res, # set input DPI (dots per inch)
+ "-dBATCH", # exit after processing
+ "-dNOPAUSE", # don't pause between pages
+ "-dSAFER", # safe mode
+ f"-sDEVICE={device}",
+ f"-sOutputFile={outfile}", # output file
+ # adjust for image origin
+ "-c",
+ f"{-bbox[0]} {-bbox[1]} translate",
+ "-f",
+ infile, # input file
+ # showpage (see https://bugs.ghostscript.com/show_bug.cgi?id=698272)
+ "-c",
+ "showpage",
+ ]
+
+ if gs_windows_binary is not None:
+ if not gs_windows_binary:
+ raise OSError("Unable to locate Ghostscript on paths")
+ command[0] = gs_windows_binary
+
+ # push data through Ghostscript
+ try:
+ startupinfo = None
+ if sys.platform.startswith("win"):
+ startupinfo = subprocess.STARTUPINFO()
+ startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
+ subprocess.check_call(command, startupinfo=startupinfo)
+ out_im = Image.open(outfile)
+ out_im.load()
+ finally:
+ try:
+ os.unlink(outfile)
+ if infile_temp:
+ os.unlink(infile_temp)
+ except OSError:
+ pass
+
+ im = out_im.im.copy()
+ out_im.close()
+ return im
+
+
+class PSFile:
+ """
+ Wrapper for bytesio object that treats either CR or LF as end of line.
+ """
+
+ def __init__(self, fp):
+ self.fp = fp
+ self.char = None
+
+ def seek(self, offset, whence=io.SEEK_SET):
+ self.char = None
+ self.fp.seek(offset, whence)
+
+ def readline(self):
+ s = [self.char or b""]
+ self.char = None
+
+ c = self.fp.read(1)
+ while (c not in b"\r\n") and len(c):
+ s.append(c)
+ c = self.fp.read(1)
+
+ self.char = self.fp.read(1)
+ # line endings can be 1 or 2 of \r \n, in either order
+ if self.char in b"\r\n":
+ self.char = None
+
+ return b"".join(s).decode("latin-1")
+
+
+def _accept(prefix):
+ return prefix[:4] == b"%!PS" or (len(prefix) >= 4 and i32(prefix) == 0xC6D3D0C5)
+
+
+##
+# Image plugin for Encapsulated PostScript. This plugin supports only
+# a few variants of this format.
+
+
+class EpsImageFile(ImageFile.ImageFile):
+ """EPS File Parser for the Python Imaging Library"""
+
+ format = "EPS"
+ format_description = "Encapsulated Postscript"
+
+ mode_map = {1: "L", 2: "LAB", 3: "RGB", 4: "CMYK"}
+
+ def _open(self):
+ (length, offset) = self._find_offset(self.fp)
+
+ # Rewrap the open file pointer in something that will
+ # convert line endings and decode to latin-1.
+ fp = PSFile(self.fp)
+
+ # go to offset - start of "%!PS"
+ fp.seek(offset)
+
+ box = None
+
+ self.mode = "RGB"
+ self._size = 1, 1 # FIXME: huh?
+
+ #
+ # Load EPS header
+
+ s_raw = fp.readline()
+ s = s_raw.strip("\r\n")
+
+ while s_raw:
+ if s:
+ if len(s) > 255:
+ raise SyntaxError("not an EPS file")
+
+ try:
+ m = split.match(s)
+ except re.error as e:
+ raise SyntaxError("not an EPS file") from e
+
+ if m:
+ k, v = m.group(1, 2)
+ self.info[k] = v
+ if k == "BoundingBox":
+ try:
+ # Note: The DSC spec says that BoundingBox
+ # fields should be integers, but some drivers
+ # put floating point values there anyway.
+ box = [int(float(i)) for i in v.split()]
+ self._size = box[2] - box[0], box[3] - box[1]
+ self.tile = [
+ ("eps", (0, 0) + self.size, offset, (length, box))
+ ]
+ except Exception:
+ pass
+
+ else:
+ m = field.match(s)
+ if m:
+ k = m.group(1)
+
+ if k == "EndComments":
+ break
+ if k[:8] == "PS-Adobe":
+ self.info[k[:8]] = k[9:]
+ else:
+ self.info[k] = ""
+ elif s[0] == "%":
+ # handle non-DSC PostScript comments that some
+ # tools mistakenly put in the Comments section
+ pass
+ else:
+ raise OSError("bad EPS header")
+
+ s_raw = fp.readline()
+ s = s_raw.strip("\r\n")
+
+ if s and s[:1] != "%":
+ break
+
+ #
+ # Scan for an "ImageData" descriptor
+
+ while s[:1] == "%":
+
+ if len(s) > 255:
+ raise SyntaxError("not an EPS file")
+
+ if s[:11] == "%ImageData:":
+ # Encoded bitmapped image.
+ x, y, bi, mo = s[11:].split(None, 7)[:4]
+
+ if int(bi) != 8:
+ break
+ try:
+ self.mode = self.mode_map[int(mo)]
+ except ValueError:
+ break
+
+ self._size = int(x), int(y)
+ return
+
+ s = fp.readline().strip("\r\n")
+ if not s:
+ break
+
+ if not box:
+ raise OSError("cannot determine EPS bounding box")
+
+ def _find_offset(self, fp):
+
+ s = fp.read(160)
+
+ if s[:4] == b"%!PS":
+ # for HEAD without binary preview
+ fp.seek(0, io.SEEK_END)
+ length = fp.tell()
+ offset = 0
+ elif i32(s, 0) == 0xC6D3D0C5:
+ # FIX for: Some EPS file not handled correctly / issue #302
+ # EPS can contain binary data
+ # or start directly with latin coding
+ # more info see:
+ # https://web.archive.org/web/20160528181353/http://partners.adobe.com/public/developer/en/ps/5002.EPSF_Spec.pdf
+ offset = i32(s, 4)
+ length = i32(s, 8)
+ else:
+ raise SyntaxError("not an EPS file")
+
+ return (length, offset)
+
+ def load(self, scale=1, transparency=False):
+ # Load EPS via Ghostscript
+ if self.tile:
+ self.im = Ghostscript(self.tile, self.size, self.fp, scale, transparency)
+ self.mode = self.im.mode
+ self._size = self.im.size
+ self.tile = []
+ return Image.Image.load(self)
+
+ def load_seek(self, *args, **kwargs):
+ # we can't incrementally load, so force ImageFile.parser to
+ # use our custom load method by defining this method.
+ pass
+
+
+#
+# --------------------------------------------------------------------
+
+
+def _save(im, fp, filename, eps=1):
+ """EPS Writer for the Python Imaging Library."""
+
+ #
+ # make sure image data is available
+ im.load()
+
+ #
+ # determine PostScript image mode
+ if im.mode == "L":
+ operator = (8, 1, b"image")
+ elif im.mode == "RGB":
+ operator = (8, 3, b"false 3 colorimage")
+ elif im.mode == "CMYK":
+ operator = (8, 4, b"false 4 colorimage")
+ else:
+ raise ValueError("image mode is not supported")
+
+ if eps:
+ #
+ # write EPS header
+ fp.write(b"%!PS-Adobe-3.0 EPSF-3.0\n")
+ fp.write(b"%%Creator: PIL 0.1 EpsEncode\n")
+ # fp.write("%%CreationDate: %s"...)
+ fp.write(b"%%%%BoundingBox: 0 0 %d %d\n" % im.size)
+ fp.write(b"%%Pages: 1\n")
+ fp.write(b"%%EndComments\n")
+ fp.write(b"%%Page: 1 1\n")
+ fp.write(b"%%ImageData: %d %d " % im.size)
+ fp.write(b'%d %d 0 1 1 "%s"\n' % operator)
+
+ #
+ # image header
+ fp.write(b"gsave\n")
+ fp.write(b"10 dict begin\n")
+ fp.write(b"/buf %d string def\n" % (im.size[0] * operator[1]))
+ fp.write(b"%d %d scale\n" % im.size)
+ fp.write(b"%d %d 8\n" % im.size) # <= bits
+ fp.write(b"[%d 0 0 -%d 0 %d]\n" % (im.size[0], im.size[1], im.size[1]))
+ fp.write(b"{ currentfile buf readhexstring pop } bind\n")
+ fp.write(operator[2] + b"\n")
+ if hasattr(fp, "flush"):
+ fp.flush()
+
+ ImageFile._save(im, fp, [("eps", (0, 0) + im.size, 0, None)])
+
+ fp.write(b"\n%%%%EndBinary\n")
+ fp.write(b"grestore end\n")
+ if hasattr(fp, "flush"):
+ fp.flush()
+
+
+#
+# --------------------------------------------------------------------
+
+
+Image.register_open(EpsImageFile.format, EpsImageFile, _accept)
+
+Image.register_save(EpsImageFile.format, _save)
+
+Image.register_extensions(EpsImageFile.format, [".ps", ".eps"])
+
+Image.register_mime(EpsImageFile.format, "application/postscript")
diff --git a/venv/Lib/site-packages/PIL/ExifTags.py b/venv/Lib/site-packages/PIL/ExifTags.py
new file mode 100644
index 0000000..7da2dda
--- /dev/null
+++ b/venv/Lib/site-packages/PIL/ExifTags.py
@@ -0,0 +1,331 @@
+#
+# The Python Imaging Library.
+# $Id$
+#
+# EXIF tags
+#
+# Copyright (c) 2003 by Secret Labs AB
+#
+# See the README file for information on usage and redistribution.
+#
+
+"""
+This module provides constants and clear-text names for various
+well-known EXIF tags.
+"""
+
+
+TAGS = {
+ # possibly incomplete
+ 0x0001: "InteropIndex",
+ 0x000B: "ProcessingSoftware",
+ 0x00FE: "NewSubfileType",
+ 0x00FF: "SubfileType",
+ 0x0100: "ImageWidth",
+ 0x0101: "ImageLength",
+ 0x0102: "BitsPerSample",
+ 0x0103: "Compression",
+ 0x0106: "PhotometricInterpretation",
+ 0x0107: "Thresholding",
+ 0x0108: "CellWidth",
+ 0x0109: "CellLength",
+ 0x010A: "FillOrder",
+ 0x010D: "DocumentName",
+ 0x010E: "ImageDescription",
+ 0x010F: "Make",
+ 0x0110: "Model",
+ 0x0111: "StripOffsets",
+ 0x0112: "Orientation",
+ 0x0115: "SamplesPerPixel",
+ 0x0116: "RowsPerStrip",
+ 0x0117: "StripByteCounts",
+ 0x0118: "MinSampleValue",
+ 0x0119: "MaxSampleValue",
+ 0x011A: "XResolution",
+ 0x011B: "YResolution",
+ 0x011C: "PlanarConfiguration",
+ 0x011D: "PageName",
+ 0x0120: "FreeOffsets",
+ 0x0121: "FreeByteCounts",
+ 0x0122: "GrayResponseUnit",
+ 0x0123: "GrayResponseCurve",
+ 0x0124: "T4Options",
+ 0x0125: "T6Options",
+ 0x0128: "ResolutionUnit",
+ 0x0129: "PageNumber",
+ 0x012D: "TransferFunction",
+ 0x0131: "Software",
+ 0x0132: "DateTime",
+ 0x013B: "Artist",
+ 0x013C: "HostComputer",
+ 0x013D: "Predictor",
+ 0x013E: "WhitePoint",
+ 0x013F: "PrimaryChromaticities",
+ 0x0140: "ColorMap",
+ 0x0141: "HalftoneHints",
+ 0x0142: "TileWidth",
+ 0x0143: "TileLength",
+ 0x0144: "TileOffsets",
+ 0x0145: "TileByteCounts",
+ 0x014A: "SubIFDs",
+ 0x014C: "InkSet",
+ 0x014D: "InkNames",
+ 0x014E: "NumberOfInks",
+ 0x0150: "DotRange",
+ 0x0151: "TargetPrinter",
+ 0x0152: "ExtraSamples",
+ 0x0153: "SampleFormat",
+ 0x0154: "SMinSampleValue",
+ 0x0155: "SMaxSampleValue",
+ 0x0156: "TransferRange",
+ 0x0157: "ClipPath",
+ 0x0158: "XClipPathUnits",
+ 0x0159: "YClipPathUnits",
+ 0x015A: "Indexed",
+ 0x015B: "JPEGTables",
+ 0x015F: "OPIProxy",
+ 0x0200: "JPEGProc",
+ 0x0201: "JpegIFOffset",
+ 0x0202: "JpegIFByteCount",
+ 0x0203: "JpegRestartInterval",
+ 0x0205: "JpegLosslessPredictors",
+ 0x0206: "JpegPointTransforms",
+ 0x0207: "JpegQTables",
+ 0x0208: "JpegDCTables",
+ 0x0209: "JpegACTables",
+ 0x0211: "YCbCrCoefficients",
+ 0x0212: "YCbCrSubSampling",
+ 0x0213: "YCbCrPositioning",
+ 0x0214: "ReferenceBlackWhite",
+ 0x02BC: "XMLPacket",
+ 0x1000: "RelatedImageFileFormat",
+ 0x1001: "RelatedImageWidth",
+ 0x1002: "RelatedImageLength",
+ 0x4746: "Rating",
+ 0x4749: "RatingPercent",
+ 0x800D: "ImageID",
+ 0x828D: "CFARepeatPatternDim",
+ 0x828E: "CFAPattern",
+ 0x828F: "BatteryLevel",
+ 0x8298: "Copyright",
+ 0x829A: "ExposureTime",
+ 0x829D: "FNumber",
+ 0x83BB: "IPTCNAA",
+ 0x8649: "ImageResources",
+ 0x8769: "ExifOffset",
+ 0x8773: "InterColorProfile",
+ 0x8822: "ExposureProgram",
+ 0x8824: "SpectralSensitivity",
+ 0x8825: "GPSInfo",
+ 0x8827: "ISOSpeedRatings",
+ 0x8828: "OECF",
+ 0x8829: "Interlace",
+ 0x882A: "TimeZoneOffset",
+ 0x882B: "SelfTimerMode",
+ 0x8830: "SensitivityType",
+ 0x8831: "StandardOutputSensitivity",
+ 0x8832: "RecommendedExposureIndex",
+ 0x8833: "ISOSpeed",
+ 0x8834: "ISOSpeedLatitudeyyy",
+ 0x8835: "ISOSpeedLatitudezzz",
+ 0x9000: "ExifVersion",
+ 0x9003: "DateTimeOriginal",
+ 0x9004: "DateTimeDigitized",
+ 0x9010: "OffsetTime",
+ 0x9011: "OffsetTimeOriginal",
+ 0x9012: "OffsetTimeDigitized",
+ 0x9101: "ComponentsConfiguration",
+ 0x9102: "CompressedBitsPerPixel",
+ 0x9201: "ShutterSpeedValue",
+ 0x9202: "ApertureValue",
+ 0x9203: "BrightnessValue",
+ 0x9204: "ExposureBiasValue",
+ 0x9205: "MaxApertureValue",
+ 0x9206: "SubjectDistance",
+ 0x9207: "MeteringMode",
+ 0x9208: "LightSource",
+ 0x9209: "Flash",
+ 0x920A: "FocalLength",
+ 0x920B: "FlashEnergy",
+ 0x920C: "SpatialFrequencyResponse",
+ 0x920D: "Noise",
+ 0x9211: "ImageNumber",
+ 0x9212: "SecurityClassification",
+ 0x9213: "ImageHistory",
+ 0x9214: "SubjectLocation",
+ 0x9215: "ExposureIndex",
+ 0x9216: "TIFF/EPStandardID",
+ 0x927C: "MakerNote",
+ 0x9286: "UserComment",
+ 0x9290: "SubsecTime",
+ 0x9291: "SubsecTimeOriginal",
+ 0x9292: "SubsecTimeDigitized",
+ 0x9400: "AmbientTemperature",
+ 0x9401: "Humidity",
+ 0x9402: "Pressure",
+ 0x9403: "WaterDepth",
+ 0x9404: "Acceleration",
+ 0x9405: "CameraElevationAngle",
+ 0x9C9B: "XPTitle",
+ 0x9C9C: "XPComment",
+ 0x9C9D: "XPAuthor",
+ 0x9C9E: "XPKeywords",
+ 0x9C9F: "XPSubject",
+ 0xA000: "FlashPixVersion",
+ 0xA001: "ColorSpace",
+ 0xA002: "ExifImageWidth",
+ 0xA003: "ExifImageHeight",
+ 0xA004: "RelatedSoundFile",
+ 0xA005: "ExifInteroperabilityOffset",
+ 0xA20B: "FlashEnergy",
+ 0xA20C: "SpatialFrequencyResponse",
+ 0xA20E: "FocalPlaneXResolution",
+ 0xA20F: "FocalPlaneYResolution",
+ 0xA210: "FocalPlaneResolutionUnit",
+ 0xA214: "SubjectLocation",
+ 0xA215: "ExposureIndex",
+ 0xA217: "SensingMethod",
+ 0xA300: "FileSource",
+ 0xA301: "SceneType",
+ 0xA302: "CFAPattern",
+ 0xA401: "CustomRendered",
+ 0xA402: "ExposureMode",
+ 0xA403: "WhiteBalance",
+ 0xA404: "DigitalZoomRatio",
+ 0xA405: "FocalLengthIn35mmFilm",
+ 0xA406: "SceneCaptureType",
+ 0xA407: "GainControl",
+ 0xA408: "Contrast",
+ 0xA409: "Saturation",
+ 0xA40A: "Sharpness",
+ 0xA40B: "DeviceSettingDescription",
+ 0xA40C: "SubjectDistanceRange",
+ 0xA420: "ImageUniqueID",
+ 0xA430: "CameraOwnerName",
+ 0xA431: "BodySerialNumber",
+ 0xA432: "LensSpecification",
+ 0xA433: "LensMake",
+ 0xA434: "LensModel",
+ 0xA435: "LensSerialNumber",
+ 0xA460: "CompositeImage",
+ 0xA461: "CompositeImageCount",
+ 0xA462: "CompositeImageExposureTimes",
+ 0xA500: "Gamma",
+ 0xC4A5: "PrintImageMatching",
+ 0xC612: "DNGVersion",
+ 0xC613: "DNGBackwardVersion",
+ 0xC614: "UniqueCameraModel",
+ 0xC615: "LocalizedCameraModel",
+ 0xC616: "CFAPlaneColor",
+ 0xC617: "CFALayout",
+ 0xC618: "LinearizationTable",
+ 0xC619: "BlackLevelRepeatDim",
+ 0xC61A: "BlackLevel",
+ 0xC61B: "BlackLevelDeltaH",
+ 0xC61C: "BlackLevelDeltaV",
+ 0xC61D: "WhiteLevel",
+ 0xC61E: "DefaultScale",
+ 0xC61F: "DefaultCropOrigin",
+ 0xC620: "DefaultCropSize",
+ 0xC621: "ColorMatrix1",
+ 0xC622: "ColorMatrix2",
+ 0xC623: "CameraCalibration1",
+ 0xC624: "CameraCalibration2",
+ 0xC625: "ReductionMatrix1",
+ 0xC626: "ReductionMatrix2",
+ 0xC627: "AnalogBalance",
+ 0xC628: "AsShotNeutral",
+ 0xC629: "AsShotWhiteXY",
+ 0xC62A: "BaselineExposure",
+ 0xC62B: "BaselineNoise",
+ 0xC62C: "BaselineSharpness",
+ 0xC62D: "BayerGreenSplit",
+ 0xC62E: "LinearResponseLimit",
+ 0xC62F: "CameraSerialNumber",
+ 0xC630: "LensInfo",
+ 0xC631: "ChromaBlurRadius",
+ 0xC632: "AntiAliasStrength",
+ 0xC633: "ShadowScale",
+ 0xC634: "DNGPrivateData",
+ 0xC635: "MakerNoteSafety",
+ 0xC65A: "CalibrationIlluminant1",
+ 0xC65B: "CalibrationIlluminant2",
+ 0xC65C: "BestQualityScale",
+ 0xC65D: "RawDataUniqueID",
+ 0xC68B: "OriginalRawFileName",
+ 0xC68C: "OriginalRawFileData",
+ 0xC68D: "ActiveArea",
+ 0xC68E: "MaskedAreas",
+ 0xC68F: "AsShotICCProfile",
+ 0xC690: "AsShotPreProfileMatrix",
+ 0xC691: "CurrentICCProfile",
+ 0xC692: "CurrentPreProfileMatrix",
+ 0xC6BF: "ColorimetricReference",
+ 0xC6F3: "CameraCalibrationSignature",
+ 0xC6F4: "ProfileCalibrationSignature",
+ 0xC6F6: "AsShotProfileName",
+ 0xC6F7: "NoiseReductionApplied",
+ 0xC6F8: "ProfileName",
+ 0xC6F9: "ProfileHueSatMapDims",
+ 0xC6FA: "ProfileHueSatMapData1",
+ 0xC6FB: "ProfileHueSatMapData2",
+ 0xC6FC: "ProfileToneCurve",
+ 0xC6FD: "ProfileEmbedPolicy",
+ 0xC6FE: "ProfileCopyright",
+ 0xC714: "ForwardMatrix1",
+ 0xC715: "ForwardMatrix2",
+ 0xC716: "PreviewApplicationName",
+ 0xC717: "PreviewApplicationVersion",
+ 0xC718: "PreviewSettingsName",
+ 0xC719: "PreviewSettingsDigest",
+ 0xC71A: "PreviewColorSpace",
+ 0xC71B: "PreviewDateTime",
+ 0xC71C: "RawImageDigest",
+ 0xC71D: "OriginalRawFileDigest",
+ 0xC71E: "SubTileBlockSize",
+ 0xC71F: "RowInterleaveFactor",
+ 0xC725: "ProfileLookTableDims",
+ 0xC726: "ProfileLookTableData",
+ 0xC740: "OpcodeList1",
+ 0xC741: "OpcodeList2",
+ 0xC74E: "OpcodeList3",
+ 0xC761: "NoiseProfile",
+}
+"""Maps EXIF tags to tag names."""
+
+
+GPSTAGS = {
+ 0: "GPSVersionID",
+ 1: "GPSLatitudeRef",
+ 2: "GPSLatitude",
+ 3: "GPSLongitudeRef",
+ 4: "GPSLongitude",
+ 5: "GPSAltitudeRef",
+ 6: "GPSAltitude",
+ 7: "GPSTimeStamp",
+ 8: "GPSSatellites",
+ 9: "GPSStatus",
+ 10: "GPSMeasureMode",
+ 11: "GPSDOP",
+ 12: "GPSSpeedRef",
+ 13: "GPSSpeed",
+ 14: "GPSTrackRef",
+ 15: "GPSTrack",
+ 16: "GPSImgDirectionRef",
+ 17: "GPSImgDirection",
+ 18: "GPSMapDatum",
+ 19: "GPSDestLatitudeRef",
+ 20: "GPSDestLatitude",
+ 21: "GPSDestLongitudeRef",
+ 22: "GPSDestLongitude",
+ 23: "GPSDestBearingRef",
+ 24: "GPSDestBearing",
+ 25: "GPSDestDistanceRef",
+ 26: "GPSDestDistance",
+ 27: "GPSProcessingMethod",
+ 28: "GPSAreaInformation",
+ 29: "GPSDateStamp",
+ 30: "GPSDifferential",
+ 31: "GPSHPositioningError",
+}
+"""Maps EXIF GPS tags to tag names."""
diff --git a/venv/Lib/site-packages/PIL/FitsImagePlugin.py b/venv/Lib/site-packages/PIL/FitsImagePlugin.py
new file mode 100644
index 0000000..c16300e
--- /dev/null
+++ b/venv/Lib/site-packages/PIL/FitsImagePlugin.py
@@ -0,0 +1,71 @@
+#
+# The Python Imaging Library
+# $Id$
+#
+# FITS file handling
+#
+# Copyright (c) 1998-2003 by Fredrik Lundh
+#
+# See the README file for information on usage and redistribution.
+#
+
+import math
+
+from . import Image, ImageFile
+
+
+def _accept(prefix):
+ return prefix[:6] == b"SIMPLE"
+
+
+class FitsImageFile(ImageFile.ImageFile):
+
+ format = "FITS"
+ format_description = "FITS"
+
+ def _open(self):
+ headers = {}
+ while True:
+ header = self.fp.read(80)
+ if not header:
+ raise OSError("Truncated FITS file")
+ keyword = header[:8].strip()
+ if keyword == b"END":
+ break
+ value = header[8:].strip()
+ if value.startswith(b"="):
+ value = value[1:].strip()
+ if not headers and (not _accept(keyword) or value != b"T"):
+ raise SyntaxError("Not a FITS file")
+ headers[keyword] = value
+
+ naxis = int(headers[b"NAXIS"])
+ if naxis == 0:
+ raise ValueError("No image data")
+ elif naxis == 1:
+ self._size = 1, int(headers[b"NAXIS1"])
+ else:
+ self._size = int(headers[b"NAXIS1"]), int(headers[b"NAXIS2"])
+
+ number_of_bits = int(headers[b"BITPIX"])
+ if number_of_bits == 8:
+ self.mode = "L"
+ elif number_of_bits == 16:
+ self.mode = "I"
+ # rawmode = "I;16S"
+ elif number_of_bits == 32:
+ self.mode = "I"
+ elif number_of_bits in (-32, -64):
+ self.mode = "F"
+ # rawmode = "F" if number_of_bits == -32 else "F;64F"
+
+ offset = math.ceil(self.fp.tell() / 2880) * 2880
+ self.tile = [("raw", (0, 0) + self.size, offset, (self.mode, 0, -1))]
+
+
+# --------------------------------------------------------------------
+# Registry
+
+Image.register_open(FitsImageFile.format, FitsImageFile, _accept)
+
+Image.register_extensions(FitsImageFile.format, [".fit", ".fits"])
diff --git a/venv/Lib/site-packages/PIL/FitsStubImagePlugin.py b/venv/Lib/site-packages/PIL/FitsStubImagePlugin.py
new file mode 100644
index 0000000..9eed029
--- /dev/null
+++ b/venv/Lib/site-packages/PIL/FitsStubImagePlugin.py
@@ -0,0 +1,77 @@
+#
+# The Python Imaging Library
+# $Id$
+#
+# FITS stub adapter
+#
+# Copyright (c) 1998-2003 by Fredrik Lundh
+#
+# See the README file for information on usage and redistribution.
+#
+
+import warnings
+
+from . import FitsImagePlugin, Image, ImageFile
+
+_handler = None
+
+
+def register_handler(handler):
+ """
+ Install application-specific FITS image handler.
+
+ :param handler: Handler object.
+ """
+ global _handler
+ _handler = handler
+
+ warnings.warn(
+ "FitsStubImagePlugin is deprecated and will be removed in Pillow "
+ "10 (2023-07-01). FITS images can now be read without a handler through "
+ "FitsImagePlugin instead.",
+ DeprecationWarning,
+ )
+
+ # Override FitsImagePlugin with this handler
+ # for backwards compatibility
+ try:
+ Image.ID.remove(FITSStubImageFile.format)
+ except ValueError:
+ pass
+
+ Image.register_open(
+ FITSStubImageFile.format, FITSStubImageFile, FitsImagePlugin._accept
+ )
+
+
+class FITSStubImageFile(ImageFile.StubImageFile):
+
+ format = FitsImagePlugin.FitsImageFile.format
+ format_description = FitsImagePlugin.FitsImageFile.format_description
+
+ def _open(self):
+ offset = self.fp.tell()
+
+ im = FitsImagePlugin.FitsImageFile(self.fp)
+ self._size = im.size
+ self.mode = im.mode
+ self.tile = []
+
+ self.fp.seek(offset)
+
+ loader = self._load()
+ if loader:
+ loader.open(self)
+
+ def _load(self):
+ return _handler
+
+
+def _save(im, fp, filename):
+ raise OSError("FITS save handler not installed")
+
+
+# --------------------------------------------------------------------
+# Registry
+
+Image.register_save(FITSStubImageFile.format, _save)
diff --git a/venv/Lib/site-packages/PIL/FliImagePlugin.py b/venv/Lib/site-packages/PIL/FliImagePlugin.py
new file mode 100644
index 0000000..ea95033
--- /dev/null
+++ b/venv/Lib/site-packages/PIL/FliImagePlugin.py
@@ -0,0 +1,171 @@
+#
+# The Python Imaging Library.
+# $Id$
+#
+# FLI/FLC file handling.
+#
+# History:
+# 95-09-01 fl Created
+# 97-01-03 fl Fixed parser, setup decoder tile
+# 98-07-15 fl Renamed offset attribute to avoid name clash
+#
+# Copyright (c) Secret Labs AB 1997-98.
+# Copyright (c) Fredrik Lundh 1995-97.
+#
+# See the README file for information on usage and redistribution.
+#
+
+
+from . import Image, ImageFile, ImagePalette
+from ._binary import i16le as i16
+from ._binary import i32le as i32
+from ._binary import o8
+
+#
+# decoder
+
+
+def _accept(prefix):
+ return (
+ len(prefix) >= 6
+ and i16(prefix, 4) in [0xAF11, 0xAF12]
+ and i16(prefix, 14) in [0, 3] # flags
+ )
+
+
+##
+# Image plugin for the FLI/FLC animation format. Use the seek
+# method to load individual frames.
+
+
+class FliImageFile(ImageFile.ImageFile):
+
+ format = "FLI"
+ format_description = "Autodesk FLI/FLC Animation"
+ _close_exclusive_fp_after_loading = False
+
+ def _open(self):
+
+ # HEAD
+ s = self.fp.read(128)
+ if not (_accept(s) and s[20:22] == b"\x00\x00"):
+ raise SyntaxError("not an FLI/FLC file")
+
+ # frames
+ self.n_frames = i16(s, 6)
+ self.is_animated = self.n_frames > 1
+
+ # image characteristics
+ self.mode = "P"
+ self._size = i16(s, 8), i16(s, 10)
+
+ # animation speed
+ duration = i32(s, 16)
+ magic = i16(s, 4)
+ if magic == 0xAF11:
+ duration = (duration * 1000) // 70
+ self.info["duration"] = duration
+
+ # look for palette
+ palette = [(a, a, a) for a in range(256)]
+
+ s = self.fp.read(16)
+
+ self.__offset = 128
+
+ if i16(s, 4) == 0xF100:
+ # prefix chunk; ignore it
+ self.__offset = self.__offset + i32(s)
+ s = self.fp.read(16)
+
+ if i16(s, 4) == 0xF1FA:
+ # look for palette chunk
+ s = self.fp.read(6)
+ if i16(s, 4) == 11:
+ self._palette(palette, 2)
+ elif i16(s, 4) == 4:
+ self._palette(palette, 0)
+
+ palette = [o8(r) + o8(g) + o8(b) for (r, g, b) in palette]
+ self.palette = ImagePalette.raw("RGB", b"".join(palette))
+
+ # set things up to decode first frame
+ self.__frame = -1
+ self.__fp = self.fp
+ self.__rewind = self.fp.tell()
+ self.seek(0)
+
+ def _palette(self, palette, shift):
+ # load palette
+
+ i = 0
+ for e in range(i16(self.fp.read(2))):
+ s = self.fp.read(2)
+ i = i + s[0]
+ n = s[1]
+ if n == 0:
+ n = 256
+ s = self.fp.read(n * 3)
+ for n in range(0, len(s), 3):
+ r = s[n] << shift
+ g = s[n + 1] << shift
+ b = s[n + 2] << shift
+ palette[i] = (r, g, b)
+ i += 1
+
+ def seek(self, frame):
+ if not self._seek_check(frame):
+ return
+ if frame < self.__frame:
+ self._seek(0)
+
+ for f in range(self.__frame + 1, frame + 1):
+ self._seek(f)
+
+ def _seek(self, frame):
+ if frame == 0:
+ self.__frame = -1
+ self.__fp.seek(self.__rewind)
+ self.__offset = 128
+ else:
+ # ensure that the previous frame was loaded
+ self.load()
+
+ if frame != self.__frame + 1:
+ raise ValueError(f"cannot seek to frame {frame}")
+ self.__frame = frame
+
+ # move to next frame
+ self.fp = self.__fp
+ self.fp.seek(self.__offset)
+
+ s = self.fp.read(4)
+ if not s:
+ raise EOFError
+
+ framesize = i32(s)
+
+ self.decodermaxblock = framesize
+ self.tile = [("fli", (0, 0) + self.size, self.__offset, None)]
+
+ self.__offset += framesize
+
+ def tell(self):
+ return self.__frame
+
+ def _close__fp(self):
+ try:
+ if self.__fp != self.fp:
+ self.__fp.close()
+ except AttributeError:
+ pass
+ finally:
+ self.__fp = None
+
+
+#
+# registry
+
+Image.register_open(FliImageFile.format, FliImageFile, _accept)
+
+Image.register_extensions(FliImageFile.format, [".fli", ".flc"])
diff --git a/venv/Lib/site-packages/PIL/FontFile.py b/venv/Lib/site-packages/PIL/FontFile.py
new file mode 100644
index 0000000..c5fc80b
--- /dev/null
+++ b/venv/Lib/site-packages/PIL/FontFile.py
@@ -0,0 +1,111 @@
+#
+# The Python Imaging Library
+# $Id$
+#
+# base class for raster font file parsers
+#
+# history:
+# 1997-06-05 fl created
+# 1997-08-19 fl restrict image width
+#
+# Copyright (c) 1997-1998 by Secret Labs AB
+# Copyright (c) 1997-1998 by Fredrik Lundh
+#
+# See the README file for information on usage and redistribution.
+#
+
+
+import os
+
+from . import Image, _binary
+
+WIDTH = 800
+
+
+def puti16(fp, values):
+ """Write network order (big-endian) 16-bit sequence"""
+ for v in values:
+ if v < 0:
+ v += 65536
+ fp.write(_binary.o16be(v))
+
+
+class FontFile:
+ """Base class for raster font file handlers."""
+
+ bitmap = None
+
+ def __init__(self):
+
+ self.info = {}
+ self.glyph = [None] * 256
+
+ def __getitem__(self, ix):
+ return self.glyph[ix]
+
+ def compile(self):
+ """Create metrics and bitmap"""
+
+ if self.bitmap:
+ return
+
+ # create bitmap large enough to hold all data
+ h = w = maxwidth = 0
+ lines = 1
+ for glyph in self:
+ if glyph:
+ d, dst, src, im = glyph
+ h = max(h, src[3] - src[1])
+ w = w + (src[2] - src[0])
+ if w > WIDTH:
+ lines += 1
+ w = src[2] - src[0]
+ maxwidth = max(maxwidth, w)
+
+ xsize = maxwidth
+ ysize = lines * h
+
+ if xsize == 0 and ysize == 0:
+ return ""
+
+ self.ysize = h
+
+ # paste glyphs into bitmap
+ self.bitmap = Image.new("1", (xsize, ysize))
+ self.metrics = [None] * 256
+ x = y = 0
+ for i in range(256):
+ glyph = self[i]
+ if glyph:
+ d, dst, src, im = glyph
+ xx = src[2] - src[0]
+ # yy = src[3] - src[1]
+ x0, y0 = x, y
+ x = x + xx
+ if x > WIDTH:
+ x, y = 0, y + h
+ x0, y0 = x, y
+ x = xx
+ s = src[0] + x0, src[1] + y0, src[2] + x0, src[3] + y0
+ self.bitmap.paste(im.crop(src), s)
+ self.metrics[i] = d, dst, s
+
+ def save(self, filename):
+ """Save font"""
+
+ self.compile()
+
+ # font data
+ self.bitmap.save(os.path.splitext(filename)[0] + ".pbm", "PNG")
+
+ # font metrics
+ with open(os.path.splitext(filename)[0] + ".pil", "wb") as fp:
+ fp.write(b"PILfont\n")
+ fp.write(f";;;;;;{self.ysize};\n".encode("ascii")) # HACK!!!
+ fp.write(b"DATA\n")
+ for id in range(256):
+ m = self.metrics[id]
+ if not m:
+ puti16(fp, [0] * 10)
+ else:
+ puti16(fp, m[0] + m[1] + m[2])
diff --git a/venv/Lib/site-packages/PIL/FpxImagePlugin.py b/venv/Lib/site-packages/PIL/FpxImagePlugin.py
new file mode 100644
index 0000000..5e38546
--- /dev/null
+++ b/venv/Lib/site-packages/PIL/FpxImagePlugin.py
@@ -0,0 +1,242 @@
+#
+# THIS IS WORK IN PROGRESS
+#
+# The Python Imaging Library.
+# $Id$
+#
+# FlashPix support for PIL
+#
+# History:
+# 97-01-25 fl Created (reads uncompressed RGB images only)
+#
+# Copyright (c) Secret Labs AB 1997.
+# Copyright (c) Fredrik Lundh 1997.
+#
+# See the README file for information on usage and redistribution.
+#
+import olefile
+
+from . import Image, ImageFile
+from ._binary import i32le as i32
+
+# we map from colour field tuples to (mode, rawmode) descriptors
+MODES = {
+ # opacity
+ (0x00007FFE): ("A", "L"),
+ # monochrome
+ (0x00010000,): ("L", "L"),
+ (0x00018000, 0x00017FFE): ("RGBA", "LA"),
+ # photo YCC
+ (0x00020000, 0x00020001, 0x00020002): ("RGB", "YCC;P"),
+ (0x00028000, 0x00028001, 0x00028002, 0x00027FFE): ("RGBA", "YCCA;P"),
+ # standard RGB (NIFRGB)
+ (0x00030000, 0x00030001, 0x00030002): ("RGB", "RGB"),
+ (0x00038000, 0x00038001, 0x00038002, 0x00037FFE): ("RGBA", "RGBA"),
+}
+
+
+#
+# --------------------------------------------------------------------
+
+
+def _accept(prefix):
+ return prefix[:8] == olefile.MAGIC
+
+
+##
+# Image plugin for the FlashPix images.
+
+
+class FpxImageFile(ImageFile.ImageFile):
+
+ format = "FPX"
+ format_description = "FlashPix"
+
+ def _open(self):
+ #
+ # read the OLE directory and see if this is a likely
+ # to be a FlashPix file
+
+ try:
+ self.ole = olefile.OleFileIO(self.fp)
+ except OSError as e:
+ raise SyntaxError("not an FPX file; invalid OLE file") from e
+
+ if self.ole.root.clsid != "56616700-C154-11CE-8553-00AA00A1F95B":
+ raise SyntaxError("not an FPX file; bad root CLSID")
+
+ self._open_index(1)
+
+ def _open_index(self, index=1):
+ #
+ # get the Image Contents Property Set
+
+ prop = self.ole.getproperties(
+ [f"Data Object Store {index:06d}", "\005Image Contents"]
+ )
+
+ # size (highest resolution)
+
+ self._size = prop[0x1000002], prop[0x1000003]
+
+ size = max(self.size)
+ i = 1
+ while size > 64:
+ size = size / 2
+ i += 1
+ self.maxid = i - 1
+
+ # mode. instead of using a single field for this, flashpix
+ # requires you to specify the mode for each channel in each
+ # resolution subimage, and leaves it to the decoder to make
+ # sure that they all match. for now, we'll cheat and assume
+ # that this is always the case.
+
+ id = self.maxid << 16
+
+ s = prop[0x2000002 | id]
+
+ colors = []
+ bands = i32(s, 4)
+ if bands > 4:
+ raise OSError("Invalid number of bands")
+ for i in range(bands):
+ # note: for now, we ignore the "uncalibrated" flag
+ colors.append(i32(s, 8 + i * 4) & 0x7FFFFFFF)
+
+ self.mode, self.rawmode = MODES[tuple(colors)]
+
+ # load JPEG tables, if any
+ self.jpeg = {}
+ for i in range(256):
+ id = 0x3000001 | (i << 16)
+ if id in prop:
+ self.jpeg[i] = prop[id]
+
+ self._open_subimage(1, self.maxid)
+
+ def _open_subimage(self, index=1, subimage=0):
+ #
+ # setup tile descriptors for a given subimage
+
+ stream = [
+ f"Data Object Store {index:06d}",
+ f"Resolution {subimage:04d}",
+ "Subimage 0000 Header",
+ ]
+
+ fp = self.ole.openstream(stream)
+
+ # skip prefix
+ fp.read(28)
+
+ # header stream
+ s = fp.read(36)
+
+ size = i32(s, 4), i32(s, 8)
+ # tilecount = i32(s, 12)
+ tilesize = i32(s, 16), i32(s, 20)
+ # channels = i32(s, 24)
+ offset = i32(s, 28)
+ length = i32(s, 32)
+
+ if size != self.size:
+ raise OSError("subimage mismatch")
+
+ # get tile descriptors
+ fp.seek(28 + offset)
+ s = fp.read(i32(s, 12) * length)
+
+ x = y = 0
+ xsize, ysize = size
+ xtile, ytile = tilesize
+ self.tile = []
+
+ for i in range(0, len(s), length):
+
+ compression = i32(s, i + 8)
+
+ if compression == 0:
+ self.tile.append(
+ (
+ "raw",
+ (x, y, x + xtile, y + ytile),
+ i32(s, i) + 28,
+ (self.rawmode),
+ )
+ )
+
+ elif compression == 1:
+
+ # FIXME: the fill decoder is not implemented
+ self.tile.append(
+ (
+ "fill",
+ (x, y, x + xtile, y + ytile),
+ i32(s, i) + 28,
+ (self.rawmode, s[12:16]),
+ )
+ )
+
+ elif compression == 2:
+
+ internal_color_conversion = s[14]
+ jpeg_tables = s[15]
+ rawmode = self.rawmode
+
+ if internal_color_conversion:
+ # The image is stored as usual (usually YCbCr).
+ if rawmode == "RGBA":
+ # For "RGBA", data is stored as YCbCrA based on
+ # negative RGB. The following trick works around
+ # this problem :
+ jpegmode, rawmode = "YCbCrK", "CMYK"
+ else:
+ jpegmode = None # let the decoder decide
+
+ else:
+ # The image is stored as defined by rawmode
+ jpegmode = rawmode
+
+ self.tile.append(
+ (
+ "jpeg",
+ (x, y, x + xtile, y + ytile),
+ i32(s, i) + 28,
+ (rawmode, jpegmode),
+ )
+ )
+
+ # FIXME: jpeg tables are tile dependent; the prefix
+ # data must be placed in the tile descriptor itself!
+
+ if jpeg_tables:
+ self.tile_prefix = self.jpeg[jpeg_tables]
+
+ else:
+ raise OSError("unknown/invalid compression")
+
+ x = x + xtile
+ if x >= xsize:
+ x, y = 0, y + ytile
+ if y >= ysize:
+ break # isn't really required
+
+ self.stream = stream
+ self.fp = None
+
+ def load(self):
+
+ if not self.fp:
+ self.fp = self.ole.openstream(self.stream[:2] + ["Subimage 0000 Data"])
+
+ return ImageFile.ImageFile.load(self)
+
+
+#
+# --------------------------------------------------------------------
+
+
+Image.register_open(FpxImageFile.format, FpxImageFile, _accept)
+
+Image.register_extension(FpxImageFile.format, ".fpx")
diff --git a/venv/Lib/site-packages/PIL/FtexImagePlugin.py b/venv/Lib/site-packages/PIL/FtexImagePlugin.py
new file mode 100644
index 0000000..55d28e1
--- /dev/null
+++ b/venv/Lib/site-packages/PIL/FtexImagePlugin.py
@@ -0,0 +1,135 @@
+"""
+A Pillow loader for .ftc and .ftu files (FTEX)
+Jerome Leclanche
+
+The contents of this file are hereby released in the public domain (CC0)
+Full text of the CC0 license:
+ https://creativecommons.org/publicdomain/zero/1.0/
+
+Independence War 2: Edge Of Chaos - Texture File Format - 16 October 2001
+
+The textures used for 3D objects in Independence War 2: Edge Of Chaos are in a
+packed custom format called FTEX. This file format uses file extensions FTC
+and FTU.
+* FTC files are compressed textures (using standard texture compression).
+* FTU files are not compressed.
+Texture File Format
+The FTC and FTU texture files both use the same format. This
+has the following structure:
+{header}
+{format_directory}
+{data}
+Where:
+{header} = {
+ u32:magic,
+ u32:version,
+ u32:width,
+ u32:height,
+ u32:mipmap_count,
+ u32:format_count
+}
+
+* The "magic" number is "FTEX".
+* "width" and "height" are the dimensions of the texture.
+* "mipmap_count" is the number of mipmaps in the texture.
+* "format_count" is the number of texture formats (different versions of the
+same texture) in this file.
+
+{format_directory} = format_count * { u32:format, u32:where }
+
+The format value is 0 for DXT1 compressed textures and 1 for 24-bit RGB
+uncompressed textures.
+The texture data for a format starts at the position "where" in the file.
+
+Each set of texture data in the file has the following structure:
+{data} = format_count * { u32:mipmap_size, mipmap_size * { u8 } }
+* "mipmap_size" is the number of bytes in that mip level. For compressed
+textures this is the size of the texture data compressed with DXT1. For 24 bit
+uncompressed textures, this is 3 * width * height. Following this are the image
+bytes for that mipmap level.
+
+Note: All data is stored in little-Endian (Intel) byte order.
+"""
+
+import struct
+import warnings
+from enum import IntEnum
+from io import BytesIO
+
+from . import Image, ImageFile
+
+MAGIC = b"FTEX"
+
+
+class Format(IntEnum):
+ DXT1 = 0
+ UNCOMPRESSED = 1
+
+
+def __getattr__(name):
+ deprecated = "deprecated and will be removed in Pillow 10 (2023-07-01). "
+ for enum, prefix in {Format: "FORMAT_"}.items():
+ if name.startswith(prefix):
+ name = name[len(prefix) :]
+ if name in enum.__members__:
+ warnings.warn(
+ prefix
+ + name
+ + " is "
+ + deprecated
+ + "Use "
+ + enum.__name__
+ + "."
+ + name
+ + " instead.",
+ DeprecationWarning,
+ stacklevel=2,
+ )
+ return enum[name]
+ raise AttributeError(f"module '{__name__}' has no attribute '{name}'")
+
+
+class FtexImageFile(ImageFile.ImageFile):
+ format = "FTEX"
+ format_description = "Texture File Format (IW2:EOC)"
+
+ def _open(self):
+ if not _accept(self.fp.read(4)):
+ raise SyntaxError("not an FTEX file")
+ struct.unpack("= 8 and i32(prefix, 0) >= 20 and i32(prefix, 4) in (1, 2)
+
+
+##
+# Image plugin for the GIMP brush format.
+
+
+class GbrImageFile(ImageFile.ImageFile):
+
+ format = "GBR"
+ format_description = "GIMP brush file"
+
+ def _open(self):
+ header_size = i32(self.fp.read(4))
+ if header_size < 20:
+ raise SyntaxError("not a GIMP brush")
+ version = i32(self.fp.read(4))
+ if version not in (1, 2):
+ raise SyntaxError(f"Unsupported GIMP brush version: {version}")
+
+ width = i32(self.fp.read(4))
+ height = i32(self.fp.read(4))
+ color_depth = i32(self.fp.read(4))
+ if width <= 0 or height <= 0:
+ raise SyntaxError("not a GIMP brush")
+ if color_depth not in (1, 4):
+ raise SyntaxError(f"Unsupported GIMP brush color depth: {color_depth}")
+
+ if version == 1:
+ comment_length = header_size - 20
+ else:
+ comment_length = header_size - 28
+ magic_number = self.fp.read(4)
+ if magic_number != b"GIMP":
+ raise SyntaxError("not a GIMP brush, bad magic number")
+ self.info["spacing"] = i32(self.fp.read(4))
+
+ comment = self.fp.read(comment_length)[:-1]
+
+ if color_depth == 1:
+ self.mode = "L"
+ else:
+ self.mode = "RGBA"
+
+ self._size = width, height
+
+ self.info["comment"] = comment
+
+ # Image might not be small
+ Image._decompression_bomb_check(self.size)
+
+ # Data is an uncompressed block of w * h * bytes/pixel
+ self._data_size = width * height * color_depth
+
+ def load(self):
+ if not self.im:
+ self.im = Image.core.new(self.mode, self.size)
+ self.frombytes(self.fp.read(self._data_size))
+ return Image.Image.load(self)
+
+
+#
+# registry
+
+
+Image.register_open(GbrImageFile.format, GbrImageFile, _accept)
+Image.register_extension(GbrImageFile.format, ".gbr")
diff --git a/venv/Lib/site-packages/PIL/GdImageFile.py b/venv/Lib/site-packages/PIL/GdImageFile.py
new file mode 100644
index 0000000..9c34ada
--- /dev/null
+++ b/venv/Lib/site-packages/PIL/GdImageFile.py
@@ -0,0 +1,90 @@
+#
+# The Python Imaging Library.
+# $Id$
+#
+# GD file handling
+#
+# History:
+# 1996-04-12 fl Created
+#
+# Copyright (c) 1997 by Secret Labs AB.
+# Copyright (c) 1996 by Fredrik Lundh.
+#
+# See the README file for information on usage and redistribution.
+#
+
+
+"""
+.. note::
+ This format cannot be automatically recognized, so the
+ class is not registered for use with :py:func:`PIL.Image.open()`. To open a
+ gd file, use the :py:func:`PIL.GdImageFile.open()` function instead.
+
+.. warning::
+ THE GD FORMAT IS NOT DESIGNED FOR DATA INTERCHANGE. This
+ implementation is provided for convenience and demonstrational
+ purposes only.
+"""
+
+
+from . import ImageFile, ImagePalette, UnidentifiedImageError
+from ._binary import i16be as i16
+from ._binary import i32be as i32
+
+
+class GdImageFile(ImageFile.ImageFile):
+ """
+ Image plugin for the GD uncompressed format. Note that this format
+ is not supported by the standard :py:func:`PIL.Image.open()` function. To use
+ this plugin, you have to import the :py:mod:`PIL.GdImageFile` module and
+ use the :py:func:`PIL.GdImageFile.open()` function.
+ """
+
+ format = "GD"
+ format_description = "GD uncompressed images"
+
+ def _open(self):
+
+ # Header
+ s = self.fp.read(1037)
+
+ if not i16(s) in [65534, 65535]:
+ raise SyntaxError("Not a valid GD 2.x .gd file")
+
+ self.mode = "L" # FIXME: "P"
+ self._size = i16(s, 2), i16(s, 4)
+
+ trueColor = s[6]
+ trueColorOffset = 2 if trueColor else 0
+
+ # transparency index
+ tindex = i32(s, 7 + trueColorOffset)
+ if tindex < 256:
+ self.info["transparency"] = tindex
+
+ self.palette = ImagePalette.raw(
+ "XBGR", s[7 + trueColorOffset + 4 : 7 + trueColorOffset + 4 + 256 * 4]
+ )
+
+ self.tile = [
+ ("raw", (0, 0) + self.size, 7 + trueColorOffset + 4 + 256 * 4, ("L", 0, 1))
+ ]
+
+
+def open(fp, mode="r"):
+ """
+ Load texture from a GD image file.
+
+ :param filename: GD file name, or an opened file handle.
+ :param mode: Optional mode. In this version, if the mode argument
+ is given, it must be "r".
+ :returns: An image instance.
+ :raises OSError: If the image could not be read.
+ """
+ if mode != "r":
+ raise ValueError("bad mode")
+
+ try:
+ return GdImageFile(fp)
+ except SyntaxError as e:
+ raise UnidentifiedImageError("cannot identify this image file") from e
diff --git a/venv/Lib/site-packages/PIL/GifImagePlugin.py b/venv/Lib/site-packages/PIL/GifImagePlugin.py
new file mode 100644
index 0000000..b798bb9
--- /dev/null
+++ b/venv/Lib/site-packages/PIL/GifImagePlugin.py
@@ -0,0 +1,1038 @@
+#
+# The Python Imaging Library.
+# $Id$
+#
+# GIF file handling
+#
+# History:
+# 1995-09-01 fl Created
+# 1996-12-14 fl Added interlace support
+# 1996-12-30 fl Added animation support
+# 1997-01-05 fl Added write support, fixed local colour map bug
+# 1997-02-23 fl Make sure to load raster data in getdata()
+# 1997-07-05 fl Support external decoder (0.4)
+# 1998-07-09 fl Handle all modes when saving (0.5)
+# 1998-07-15 fl Renamed offset attribute to avoid name clash
+# 2001-04-16 fl Added rewind support (seek to frame 0) (0.6)
+# 2001-04-17 fl Added palette optimization (0.7)
+# 2002-06-06 fl Added transparency support for save (0.8)
+# 2004-02-24 fl Disable interlacing for small images
+#
+# Copyright (c) 1997-2004 by Secret Labs AB
+# Copyright (c) 1995-2004 by Fredrik Lundh
+#
+# See the README file for information on usage and redistribution.
+#
+
+import itertools
+import math
+import os
+import subprocess
+from enum import IntEnum
+
+from . import Image, ImageChops, ImageFile, ImagePalette, ImageSequence
+from ._binary import i16le as i16
+from ._binary import o8
+from ._binary import o16le as o16
+
+
+class LoadingStrategy(IntEnum):
+ """.. versionadded:: 9.1.0"""
+
+ RGB_AFTER_FIRST = 0
+ RGB_AFTER_DIFFERENT_PALETTE_ONLY = 1
+ RGB_ALWAYS = 2
+
+
+#: .. versionadded:: 9.1.0
+LOADING_STRATEGY = LoadingStrategy.RGB_AFTER_FIRST
+
+# --------------------------------------------------------------------
+# Identify/read GIF files
+
+
+def _accept(prefix):
+ return prefix[:6] in [b"GIF87a", b"GIF89a"]
+
+
+##
+# Image plugin for GIF images. This plugin supports both GIF87 and
+# GIF89 images.
+
+
+class GifImageFile(ImageFile.ImageFile):
+
+ format = "GIF"
+ format_description = "Compuserve GIF"
+ _close_exclusive_fp_after_loading = False
+
+ global_palette = None
+
+ def data(self):
+ s = self.fp.read(1)
+ if s and s[0]:
+ return self.fp.read(s[0])
+ return None
+
+ def _is_palette_needed(self, p):
+ for i in range(0, len(p), 3):
+ if not (i // 3 == p[i] == p[i + 1] == p[i + 2]):
+ return True
+ return False
+
+ def _open(self):
+
+ # Screen
+ s = self.fp.read(13)
+ if not _accept(s):
+ raise SyntaxError("not a GIF file")
+
+ self.info["version"] = s[:6]
+ self._size = i16(s, 6), i16(s, 8)
+ self.tile = []
+ flags = s[10]
+ bits = (flags & 7) + 1
+
+ if flags & 128:
+ # get global palette
+ self.info["background"] = s[11]
+ # check if palette contains colour indices
+ p = self.fp.read(3 << bits)
+ if self._is_palette_needed(p):
+ p = ImagePalette.raw("RGB", p)
+ self.global_palette = self.palette = p
+
+ self.__fp = self.fp # FIXME: hack
+ self.__rewind = self.fp.tell()
+ self._n_frames = None
+ self._is_animated = None
+ self._seek(0) # get ready to read first frame
+
+ @property
+ def n_frames(self):
+ if self._n_frames is None:
+ current = self.tell()
+ try:
+ while True:
+ self._seek(self.tell() + 1, False)
+ except EOFError:
+ self._n_frames = self.tell() + 1
+ self.seek(current)
+ return self._n_frames
+
+ @property
+ def is_animated(self):
+ if self._is_animated is None:
+ if self._n_frames is not None:
+ self._is_animated = self._n_frames != 1
+ else:
+ current = self.tell()
+ if current:
+ self._is_animated = True
+ else:
+ try:
+ self._seek(1, False)
+ self._is_animated = True
+ except EOFError:
+ self._is_animated = False
+
+ self.seek(current)
+ return self._is_animated
+
+ def seek(self, frame):
+ if not self._seek_check(frame):
+ return
+ if frame < self.__frame:
+ self.im = None
+ self._seek(0)
+
+ last_frame = self.__frame
+ for f in range(self.__frame + 1, frame + 1):
+ try:
+ self._seek(f)
+ except EOFError as e:
+ self.seek(last_frame)
+ raise EOFError("no more images in GIF file") from e
+
+ def _seek(self, frame, update_image=True):
+
+ if frame == 0:
+ # rewind
+ self.__offset = 0
+ self.dispose = None
+ self.__frame = -1
+ self.__fp.seek(self.__rewind)
+ self.disposal_method = 0
+ else:
+ # ensure that the previous frame was loaded
+ if self.tile and update_image:
+ self.load()
+
+ if frame != self.__frame + 1:
+ raise ValueError(f"cannot seek to frame {frame}")
+
+ self.fp = self.__fp
+ if self.__offset:
+ # backup to last frame
+ self.fp.seek(self.__offset)
+ while self.data():
+ pass
+ self.__offset = 0
+
+ s = self.fp.read(1)
+ if not s or s == b";":
+ raise EOFError
+
+ self.__frame = frame
+
+ self.tile = []
+
+ palette = None
+
+ info = {}
+ frame_transparency = None
+ interlace = None
+ frame_dispose_extent = None
+ while True:
+
+ if not s:
+ s = self.fp.read(1)
+ if not s or s == b";":
+ break
+
+ elif s == b"!":
+ #
+ # extensions
+ #
+ s = self.fp.read(1)
+ block = self.data()
+ if s[0] == 249:
+ #
+ # graphic control extension
+ #
+ flags = block[0]
+ if flags & 1:
+ frame_transparency = block[3]
+ info["duration"] = i16(block, 1) * 10
+
+ # disposal method - find the value of bits 4 - 6
+ dispose_bits = 0b00011100 & flags
+ dispose_bits = dispose_bits >> 2
+ if dispose_bits:
+ # only set the dispose if it is not
+ # unspecified. I'm not sure if this is
+ # correct, but it seems to prevent the last
+ # frame from looking odd for some animations
+ self.disposal_method = dispose_bits
+ elif s[0] == 254:
+ #
+ # comment extension
+ #
+ while block:
+ if "comment" in info:
+ info["comment"] += block
+ else:
+ info["comment"] = block
+ block = self.data()
+ s = None
+ continue
+ elif s[0] == 255:
+ #
+ # application extension
+ #
+ info["extension"] = block, self.fp.tell()
+ if block[:11] == b"NETSCAPE2.0":
+ block = self.data()
+ if len(block) >= 3 and block[0] == 1:
+ info["loop"] = i16(block, 1)
+ while self.data():
+ pass
+
+ elif s == b",":
+ #
+ # local image
+ #
+ s = self.fp.read(9)
+
+ # extent
+ x0, y0 = i16(s, 0), i16(s, 2)
+ x1, y1 = x0 + i16(s, 4), y0 + i16(s, 6)
+ if (x1 > self.size[0] or y1 > self.size[1]) and update_image:
+ self._size = max(x1, self.size[0]), max(y1, self.size[1])
+ frame_dispose_extent = x0, y0, x1, y1
+ flags = s[8]
+
+ interlace = (flags & 64) != 0
+
+ if flags & 128:
+ bits = (flags & 7) + 1
+ p = self.fp.read(3 << bits)
+ if self._is_palette_needed(p):
+ palette = ImagePalette.raw("RGB", p)
+
+ # image data
+ bits = self.fp.read(1)[0]
+ self.__offset = self.fp.tell()
+ break
+
+ else:
+ pass
+ # raise OSError, "illegal GIF tag `%x`" % s[0]
+ s = None
+
+ if interlace is None:
+ # self.__fp = None
+ raise EOFError
+ if not update_image:
+ return
+
+ if self.dispose:
+ self.im.paste(self.dispose, self.dispose_extent)
+
+ self._frame_palette = palette or self.global_palette
+ if frame == 0:
+ if self._frame_palette:
+ self.mode = (
+ "RGB" if LOADING_STRATEGY == LoadingStrategy.RGB_ALWAYS else "P"
+ )
+ else:
+ self.mode = "L"
+
+ if not palette and self.global_palette:
+ from copy import copy
+
+ palette = copy(self.global_palette)
+ self.palette = palette
+ else:
+ self._frame_transparency = frame_transparency
+ if self.mode == "P":
+ if (
+ LOADING_STRATEGY != LoadingStrategy.RGB_AFTER_DIFFERENT_PALETTE_ONLY
+ or palette
+ ):
+ self.pyaccess = None
+ if "transparency" in self.info:
+ self.im.putpalettealpha(self.info["transparency"], 0)
+ self.im = self.im.convert("RGBA", Image.Dither.FLOYDSTEINBERG)
+ self.mode = "RGBA"
+ del self.info["transparency"]
+ else:
+ self.mode = "RGB"
+ self.im = self.im.convert("RGB", Image.Dither.FLOYDSTEINBERG)
+
+ def _rgb(color):
+ if self._frame_palette:
+ color = tuple(self._frame_palette.palette[color * 3 : color * 3 + 3])
+ else:
+ color = (color, color, color)
+ return color
+
+ self.dispose_extent = frame_dispose_extent
+ try:
+ if self.disposal_method < 2:
+ # do not dispose or none specified
+ self.dispose = None
+ elif self.disposal_method == 2:
+ # replace with background colour
+
+ # only dispose the extent in this frame
+ x0, y0, x1, y1 = self.dispose_extent
+ dispose_size = (x1 - x0, y1 - y0)
+
+ Image._decompression_bomb_check(dispose_size)
+
+ # by convention, attempt to use transparency first
+ dispose_mode = "P"
+ color = self.info.get("transparency", frame_transparency)
+ if color is not None:
+ if self.mode in ("RGB", "RGBA"):
+ dispose_mode = "RGBA"
+ color = _rgb(color) + (0,)
+ else:
+ color = self.info.get("background", 0)
+ if self.mode in ("RGB", "RGBA"):
+ dispose_mode = "RGB"
+ color = _rgb(color)
+ self.dispose = Image.core.fill(dispose_mode, dispose_size, color)
+ else:
+ # replace with previous contents
+ if self.im is not None:
+ # only dispose the extent in this frame
+ self.dispose = self._crop(self.im, self.dispose_extent)
+ elif frame_transparency is not None:
+ x0, y0, x1, y1 = self.dispose_extent
+ dispose_size = (x1 - x0, y1 - y0)
+
+ Image._decompression_bomb_check(dispose_size)
+ dispose_mode = "P"
+ color = frame_transparency
+ if self.mode in ("RGB", "RGBA"):
+ dispose_mode = "RGBA"
+ color = _rgb(frame_transparency) + (0,)
+ self.dispose = Image.core.fill(dispose_mode, dispose_size, color)
+ except AttributeError:
+ pass
+
+ if interlace is not None:
+ transparency = -1
+ if frame_transparency is not None:
+ if frame == 0:
+ self.info["transparency"] = frame_transparency
+ elif self.mode not in ("RGB", "RGBA"):
+ transparency = frame_transparency
+ self.tile = [
+ (
+ "gif",
+ (x0, y0, x1, y1),
+ self.__offset,
+ (bits, interlace, transparency),
+ )
+ ]
+
+ for k in ["duration", "comment", "extension", "loop"]:
+ if k in info:
+ self.info[k] = info[k]
+ elif k in self.info:
+ del self.info[k]
+
+ def load_prepare(self):
+ temp_mode = "P" if self._frame_palette else "L"
+ self._prev_im = None
+ if self.__frame == 0:
+ if "transparency" in self.info:
+ self.im = Image.core.fill(
+ temp_mode, self.size, self.info["transparency"]
+ )
+ elif self.mode in ("RGB", "RGBA"):
+ self._prev_im = self.im
+ if self._frame_palette:
+ self.im = Image.core.fill("P", self.size, self._frame_transparency or 0)
+ self.im.putpalette(*self._frame_palette.getdata())
+ else:
+ self.im = None
+ self.mode = temp_mode
+ self._frame_palette = None
+
+ super().load_prepare()
+
+ def load_end(self):
+ if self.__frame == 0:
+ if self.mode == "P" and LOADING_STRATEGY == LoadingStrategy.RGB_ALWAYS:
+ self.mode = "RGB"
+ self.im = self.im.convert("RGB", Image.Dither.FLOYDSTEINBERG)
+ return
+ if self.mode == "P" and self._prev_im:
+ if self._frame_transparency is not None:
+ self.im.putpalettealpha(self._frame_transparency, 0)
+ frame_im = self.im.convert("RGBA")
+ else:
+ frame_im = self.im.convert("RGB")
+ else:
+ if not self._prev_im:
+ return
+ frame_im = self.im
+ frame_im = self._crop(frame_im, self.dispose_extent)
+
+ self.im = self._prev_im
+ self.mode = self.im.mode
+ if frame_im.mode == "RGBA":
+ self.im.paste(frame_im, self.dispose_extent, frame_im)
+ else:
+ self.im.paste(frame_im, self.dispose_extent)
+
+ def tell(self):
+ return self.__frame
+
+ def _close__fp(self):
+ try:
+ if self.__fp != self.fp:
+ self.__fp.close()
+ except AttributeError:
+ pass
+ finally:
+ self.__fp = None
+
+
+# --------------------------------------------------------------------
+# Write GIF files
+
+
+RAWMODE = {"1": "L", "L": "L", "P": "P"}
+
+
+def _normalize_mode(im):
+ """
+ Takes an image (or frame), returns an image in a mode that is appropriate
+ for saving in a Gif.
+
+ It may return the original image, or it may return an image converted to
+ palette or 'L' mode.
+
+ :param im: Image object
+ :returns: Image object
+ """
+ if im.mode in RAWMODE:
+ im.load()
+ return im
+ if Image.getmodebase(im.mode) == "RGB":
+ im = im.convert("P", palette=Image.Palette.ADAPTIVE)
+ if im.palette.mode == "RGBA":
+ for rgba in im.palette.colors.keys():
+ if rgba[3] == 0:
+ im.info["transparency"] = im.palette.colors[rgba]
+ break
+ return im
+ return im.convert("L")
+
+
+def _normalize_palette(im, palette, info):
+ """
+ Normalizes the palette for image.
+ - Sets the palette to the incoming palette, if provided.
+ - Ensures that there's a palette for L mode images
+ - Optimizes the palette if necessary/desired.
+
+ :param im: Image object
+ :param palette: bytes object containing the source palette, or ....
+ :param info: encoderinfo
+ :returns: Image object
+ """
+ source_palette = None
+ if palette:
+ # a bytes palette
+ if isinstance(palette, (bytes, bytearray, list)):
+ source_palette = bytearray(palette[:768])
+ if isinstance(palette, ImagePalette.ImagePalette):
+ source_palette = bytearray(palette.palette)
+
+ if im.mode == "P":
+ if not source_palette:
+ source_palette = im.im.getpalette("RGB")[:768]
+ else: # L-mode
+ if not source_palette:
+ source_palette = bytearray(i // 3 for i in range(768))
+ im.palette = ImagePalette.ImagePalette("RGB", palette=source_palette)
+
+ if palette:
+ used_palette_colors = []
+ for i in range(0, len(source_palette), 3):
+ source_color = tuple(source_palette[i : i + 3])
+ try:
+ index = im.palette.colors[source_color]
+ except KeyError:
+ index = None
+ used_palette_colors.append(index)
+ for i, index in enumerate(used_palette_colors):
+ if index is None:
+ for j in range(len(used_palette_colors)):
+ if j not in used_palette_colors:
+ used_palette_colors[i] = j
+ break
+ im = im.remap_palette(used_palette_colors)
+ else:
+ used_palette_colors = _get_optimize(im, info)
+ if used_palette_colors is not None:
+ return im.remap_palette(used_palette_colors, source_palette)
+
+ im.palette.palette = source_palette
+ return im
+
+
+def _write_single_frame(im, fp, palette):
+ im_out = _normalize_mode(im)
+ for k, v in im_out.info.items():
+ im.encoderinfo.setdefault(k, v)
+ im_out = _normalize_palette(im_out, palette, im.encoderinfo)
+
+ for s in _get_global_header(im_out, im.encoderinfo):
+ fp.write(s)
+
+ # local image header
+ flags = 0
+ if get_interlace(im):
+ flags = flags | 64
+ _write_local_header(fp, im, (0, 0), flags)
+
+ im_out.encoderconfig = (8, get_interlace(im))
+ ImageFile._save(im_out, fp, [("gif", (0, 0) + im.size, 0, RAWMODE[im_out.mode])])
+
+ fp.write(b"\0") # end of image data
+
+
+def _write_multiple_frames(im, fp, palette):
+
+ duration = im.encoderinfo.get("duration", im.info.get("duration"))
+ disposal = im.encoderinfo.get("disposal", im.info.get("disposal"))
+
+ im_frames = []
+ frame_count = 0
+ background_im = None
+ for imSequence in itertools.chain([im], im.encoderinfo.get("append_images", [])):
+ for im_frame in ImageSequence.Iterator(imSequence):
+ # a copy is required here since seek can still mutate the image
+ im_frame = _normalize_mode(im_frame.copy())
+ if frame_count == 0:
+ for k, v in im_frame.info.items():
+ im.encoderinfo.setdefault(k, v)
+ im_frame = _normalize_palette(im_frame, palette, im.encoderinfo)
+
+ encoderinfo = im.encoderinfo.copy()
+ if isinstance(duration, (list, tuple)):
+ encoderinfo["duration"] = duration[frame_count]
+ if isinstance(disposal, (list, tuple)):
+ encoderinfo["disposal"] = disposal[frame_count]
+ frame_count += 1
+
+ if im_frames:
+ # delta frame
+ previous = im_frames[-1]
+ if encoderinfo.get("disposal") == 2:
+ if background_im is None:
+ color = im.encoderinfo.get(
+ "transparency", im.info.get("transparency", (0, 0, 0))
+ )
+ background = _get_background(im_frame, color)
+ background_im = Image.new("P", im_frame.size, background)
+ background_im.putpalette(im_frames[0]["im"].palette)
+ base_im = background_im
+ else:
+ base_im = previous["im"]
+ if _get_palette_bytes(im_frame) == _get_palette_bytes(base_im):
+ delta = ImageChops.subtract_modulo(im_frame, base_im)
+ else:
+ delta = ImageChops.subtract_modulo(
+ im_frame.convert("RGB"), base_im.convert("RGB")
+ )
+ bbox = delta.getbbox()
+ if not bbox:
+ # This frame is identical to the previous frame
+ if duration:
+ previous["encoderinfo"]["duration"] += encoderinfo["duration"]
+ continue
+ else:
+ bbox = None
+ im_frames.append({"im": im_frame, "bbox": bbox, "encoderinfo": encoderinfo})
+
+ if len(im_frames) > 1:
+ for frame_data in im_frames:
+ im_frame = frame_data["im"]
+ if not frame_data["bbox"]:
+ # global header
+ for s in _get_global_header(im_frame, frame_data["encoderinfo"]):
+ fp.write(s)
+ offset = (0, 0)
+ else:
+ # compress difference
+ if not palette:
+ frame_data["encoderinfo"]["include_color_table"] = True
+
+ im_frame = im_frame.crop(frame_data["bbox"])
+ offset = frame_data["bbox"][:2]
+ _write_frame_data(fp, im_frame, offset, frame_data["encoderinfo"])
+ return True
+ elif "duration" in im.encoderinfo and isinstance(
+ im.encoderinfo["duration"], (list, tuple)
+ ):
+ # Since multiple frames will not be written, add together the frame durations
+ im.encoderinfo["duration"] = sum(im.encoderinfo["duration"])
+
+
+def _save_all(im, fp, filename):
+ _save(im, fp, filename, save_all=True)
+
+
+def _save(im, fp, filename, save_all=False):
+ # header
+ if "palette" in im.encoderinfo or "palette" in im.info:
+ palette = im.encoderinfo.get("palette", im.info.get("palette"))
+ else:
+ palette = None
+ im.encoderinfo["optimize"] = im.encoderinfo.get("optimize", True)
+
+ if not save_all or not _write_multiple_frames(im, fp, palette):
+ _write_single_frame(im, fp, palette)
+
+ fp.write(b";") # end of file
+
+ if hasattr(fp, "flush"):
+ fp.flush()
+
+
+def get_interlace(im):
+ interlace = im.encoderinfo.get("interlace", 1)
+
+ # workaround for @PIL153
+ if min(im.size) < 16:
+ interlace = 0
+
+ return interlace
+
+
+def _write_local_header(fp, im, offset, flags):
+ transparent_color_exists = False
+ try:
+ if "transparency" in im.encoderinfo:
+ transparency = im.encoderinfo["transparency"]
+ else:
+ transparency = im.info["transparency"]
+ transparency = int(transparency)
+ except (KeyError, ValueError):
+ pass
+ else:
+ # optimize the block away if transparent color is not used
+ transparent_color_exists = True
+
+ used_palette_colors = _get_optimize(im, im.encoderinfo)
+ if used_palette_colors is not None:
+ # adjust the transparency index after optimize
+ try:
+ transparency = used_palette_colors.index(transparency)
+ except ValueError:
+ transparent_color_exists = False
+
+ if "duration" in im.encoderinfo:
+ duration = int(im.encoderinfo["duration"] / 10)
+ else:
+ duration = 0
+
+ disposal = int(im.encoderinfo.get("disposal", 0))
+
+ if transparent_color_exists or duration != 0 or disposal:
+ packed_flag = 1 if transparent_color_exists else 0
+ packed_flag |= disposal << 2
+ if not transparent_color_exists:
+ transparency = 0
+
+ fp.write(
+ b"!"
+ + o8(249) # extension intro
+ + o8(4) # length
+ + o8(packed_flag) # packed fields
+ + o16(duration) # duration
+ + o8(transparency) # transparency index
+ + o8(0)
+ )
+
+ if "comment" in im.encoderinfo and 1 <= len(im.encoderinfo["comment"]):
+ fp.write(b"!" + o8(254)) # extension intro
+ comment = im.encoderinfo["comment"]
+ if isinstance(comment, str):
+ comment = comment.encode()
+ for i in range(0, len(comment), 255):
+ subblock = comment[i : i + 255]
+ fp.write(o8(len(subblock)) + subblock)
+ fp.write(o8(0))
+ if "loop" in im.encoderinfo:
+ number_of_loops = im.encoderinfo["loop"]
+ fp.write(
+ b"!"
+ + o8(255) # extension intro
+ + o8(11)
+ + b"NETSCAPE2.0"
+ + o8(3)
+ + o8(1)
+ + o16(number_of_loops) # number of loops
+ + o8(0)
+ )
+ include_color_table = im.encoderinfo.get("include_color_table")
+ if include_color_table:
+ palette_bytes = _get_palette_bytes(im)
+ color_table_size = _get_color_table_size(palette_bytes)
+ if color_table_size:
+ flags = flags | 128 # local color table flag
+ flags = flags | color_table_size
+
+ fp.write(
+ b","
+ + o16(offset[0]) # offset
+ + o16(offset[1])
+ + o16(im.size[0]) # size
+ + o16(im.size[1])
+ + o8(flags) # flags
+ )
+ if include_color_table and color_table_size:
+ fp.write(_get_header_palette(palette_bytes))
+ fp.write(o8(8)) # bits
+
+
+def _save_netpbm(im, fp, filename):
+
+ # Unused by default.
+ # To use, uncomment the register_save call at the end of the file.
+ #
+ # If you need real GIF compression and/or RGB quantization, you
+ # can use the external NETPBM/PBMPLUS utilities. See comments
+ # below for information on how to enable this.
+ tempfile = im._dump()
+
+ try:
+ with open(filename, "wb") as f:
+ if im.mode != "RGB":
+ subprocess.check_call(
+ ["ppmtogif", tempfile], stdout=f, stderr=subprocess.DEVNULL
+ )
+ else:
+ # Pipe ppmquant output into ppmtogif
+ # "ppmquant 256 %s | ppmtogif > %s" % (tempfile, filename)
+ quant_cmd = ["ppmquant", "256", tempfile]
+ togif_cmd = ["ppmtogif"]
+ quant_proc = subprocess.Popen(
+ quant_cmd, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL
+ )
+ togif_proc = subprocess.Popen(
+ togif_cmd,
+ stdin=quant_proc.stdout,
+ stdout=f,
+ stderr=subprocess.DEVNULL,
+ )
+
+ # Allow ppmquant to receive SIGPIPE if ppmtogif exits
+ quant_proc.stdout.close()
+
+ retcode = quant_proc.wait()
+ if retcode:
+ raise subprocess.CalledProcessError(retcode, quant_cmd)
+
+ retcode = togif_proc.wait()
+ if retcode:
+ raise subprocess.CalledProcessError(retcode, togif_cmd)
+ finally:
+ try:
+ os.unlink(tempfile)
+ except OSError:
+ pass
+
+
+# Force optimization so that we can test performance against
+# cases where it took lots of memory and time previously.
+_FORCE_OPTIMIZE = False
+
+
+def _get_optimize(im, info):
+ """
+ Palette optimization is a potentially expensive operation.
+
+ This function determines if the palette should be optimized using
+ some heuristics, then returns the list of palette entries in use.
+
+ :param im: Image object
+ :param info: encoderinfo
+ :returns: list of indexes of palette entries in use, or None
+ """
+ if im.mode in ("P", "L") and info and info.get("optimize", 0):
+ # Potentially expensive operation.
+
+ # The palette saves 3 bytes per color not used, but palette
+ # lengths are restricted to 3*(2**N) bytes. Max saving would
+ # be 768 -> 6 bytes if we went all the way down to 2 colors.
+ # * If we're over 128 colors, we can't save any space.
+ # * If there aren't any holes, it's not worth collapsing.
+ # * If we have a 'large' image, the palette is in the noise.
+
+ # create the new palette if not every color is used
+ optimise = _FORCE_OPTIMIZE or im.mode == "L"
+ if optimise or im.width * im.height < 512 * 512:
+ # check which colors are used
+ used_palette_colors = []
+ for i, count in enumerate(im.histogram()):
+ if count:
+ used_palette_colors.append(i)
+
+ if optimise or (
+ len(used_palette_colors) <= 128
+ and max(used_palette_colors) > len(used_palette_colors)
+ ):
+ return used_palette_colors
+
+
+def _get_color_table_size(palette_bytes):
+ # calculate the palette size for the header
+ if not palette_bytes:
+ return 0
+ elif len(palette_bytes) < 9:
+ return 1
+ else:
+ return math.ceil(math.log(len(palette_bytes) // 3, 2)) - 1
+
+
+def _get_header_palette(palette_bytes):
+ """
+ Returns the palette, null padded to the next power of 2 (*3) bytes
+ suitable for direct inclusion in the GIF header
+
+ :param palette_bytes: Unpadded palette bytes, in RGBRGB form
+ :returns: Null padded palette
+ """
+ color_table_size = _get_color_table_size(palette_bytes)
+
+ # add the missing amount of bytes
+ # the palette has to be 2< 0:
+ palette_bytes += o8(0) * 3 * actual_target_size_diff
+ return palette_bytes
+
+
+def _get_palette_bytes(im):
+ """
+ Gets the palette for inclusion in the gif header
+
+ :param im: Image object
+ :returns: Bytes, len<=768 suitable for inclusion in gif header
+ """
+ return im.palette.palette
+
+
+def _get_background(im, infoBackground):
+ background = 0
+ if infoBackground:
+ background = infoBackground
+ if isinstance(background, tuple):
+ # WebPImagePlugin stores an RGBA value in info["background"]
+ # So it must be converted to the same format as GifImagePlugin's
+ # info["background"] - a global color table index
+ try:
+ background = im.palette.getcolor(background, im)
+ except ValueError as e:
+ if str(e) == "cannot allocate more than 256 colors":
+ # If all 256 colors are in use,
+ # then there is no need for the background color
+ return 0
+ else:
+ raise
+ return background
+
+
+def _get_global_header(im, info):
+ """Return a list of strings representing a GIF header"""
+
+ # Header Block
+ # https://www.matthewflickinger.com/lab/whatsinagif/bits_and_bytes.asp
+
+ version = b"87a"
+ for extensionKey in ["transparency", "duration", "loop", "comment"]:
+ if info and extensionKey in info:
+ if (extensionKey == "duration" and info[extensionKey] == 0) or (
+ extensionKey == "comment" and not (1 <= len(info[extensionKey]) <= 255)
+ ):
+ continue
+ version = b"89a"
+ break
+ else:
+ if im.info.get("version") == b"89a":
+ version = b"89a"
+
+ background = _get_background(im, info.get("background"))
+
+ palette_bytes = _get_palette_bytes(im)
+ color_table_size = _get_color_table_size(palette_bytes)
+
+ return [
+ b"GIF" # signature
+ + version # version
+ + o16(im.size[0]) # canvas width
+ + o16(im.size[1]), # canvas height
+ # Logical Screen Descriptor
+ # size of global color table + global color table flag
+ o8(color_table_size + 128), # packed fields
+ # background + reserved/aspect
+ o8(background) + o8(0),
+ # Global Color Table
+ _get_header_palette(palette_bytes),
+ ]
+
+
+def _write_frame_data(fp, im_frame, offset, params):
+ try:
+ im_frame.encoderinfo = params
+
+ # local image header
+ _write_local_header(fp, im_frame, offset, 0)
+
+ ImageFile._save(
+ im_frame, fp, [("gif", (0, 0) + im_frame.size, 0, RAWMODE[im_frame.mode])]
+ )
+
+ fp.write(b"\0") # end of image data
+ finally:
+ del im_frame.encoderinfo
+
+
+# --------------------------------------------------------------------
+# Legacy GIF utilities
+
+
+def getheader(im, palette=None, info=None):
+ """
+ Legacy Method to get Gif data from image.
+
+ Warning:: May modify image data.
+
+ :param im: Image object
+ :param palette: bytes object containing the source palette, or ....
+ :param info: encoderinfo
+ :returns: tuple of(list of header items, optimized palette)
+
+ """
+ used_palette_colors = _get_optimize(im, info)
+
+ if info is None:
+ info = {}
+
+ if "background" not in info and "background" in im.info:
+ info["background"] = im.info["background"]
+
+ im_mod = _normalize_palette(im, palette, info)
+ im.palette = im_mod.palette
+ im.im = im_mod.im
+ header = _get_global_header(im, info)
+
+ return header, used_palette_colors
+
+
+# To specify duration, add the time in milliseconds to getdata(),
+# e.g. getdata(im_frame, duration=1000)
+def getdata(im, offset=(0, 0), **params):
+ """
+ Legacy Method
+
+ Return a list of strings representing this image.
+ The first string is a local image header, the rest contains
+ encoded image data.
+
+ :param im: Image object
+ :param offset: Tuple of (x, y) pixels. Defaults to (0,0)
+ :param \\**params: E.g. duration or other encoder info parameters
+ :returns: List of Bytes containing gif encoded frame data
+
+ """
+
+ class Collector:
+ data = []
+
+ def write(self, data):
+ self.data.append(data)
+
+ im.load() # make sure raster data is available
+
+ fp = Collector()
+
+ _write_frame_data(fp, im, offset, params)
+
+ return fp.data
+
+
+# --------------------------------------------------------------------
+# Registry
+
+Image.register_open(GifImageFile.format, GifImageFile, _accept)
+Image.register_save(GifImageFile.format, _save)
+Image.register_save_all(GifImageFile.format, _save_all)
+Image.register_extension(GifImageFile.format, ".gif")
+Image.register_mime(GifImageFile.format, "image/gif")
+
+#
+# Uncomment the following line if you wish to use NETPBM/PBMPLUS
+# instead of the built-in "uncompressed" GIF encoder
+
+# Image.register_save(GifImageFile.format, _save_netpbm)
diff --git a/venv/Lib/site-packages/PIL/GimpGradientFile.py b/venv/Lib/site-packages/PIL/GimpGradientFile.py
new file mode 100644
index 0000000..7ab7f99
--- /dev/null
+++ b/venv/Lib/site-packages/PIL/GimpGradientFile.py
@@ -0,0 +1,140 @@
+#
+# Python Imaging Library
+# $Id$
+#
+# stuff to read (and render) GIMP gradient files
+#
+# History:
+# 97-08-23 fl Created
+#
+# Copyright (c) Secret Labs AB 1997.
+# Copyright (c) Fredrik Lundh 1997.
+#
+# See the README file for information on usage and redistribution.
+#
+
+"""
+Stuff to translate curve segments to palette values (derived from
+the corresponding code in GIMP, written by Federico Mena Quintero.
+See the GIMP distribution for more information.)
+"""
+
+
+from math import log, pi, sin, sqrt
+
+from ._binary import o8
+
+EPSILON = 1e-10
+"""""" # Enable auto-doc for data member
+
+
+def linear(middle, pos):
+ if pos <= middle:
+ if middle < EPSILON:
+ return 0.0
+ else:
+ return 0.5 * pos / middle
+ else:
+ pos = pos - middle
+ middle = 1.0 - middle
+ if middle < EPSILON:
+ return 1.0
+ else:
+ return 0.5 + 0.5 * pos / middle
+
+
+def curved(middle, pos):
+ return pos ** (log(0.5) / log(max(middle, EPSILON)))
+
+
+def sine(middle, pos):
+ return (sin((-pi / 2.0) + pi * linear(middle, pos)) + 1.0) / 2.0
+
+
+def sphere_increasing(middle, pos):
+ return sqrt(1.0 - (linear(middle, pos) - 1.0) ** 2)
+
+
+def sphere_decreasing(middle, pos):
+ return 1.0 - sqrt(1.0 - linear(middle, pos) ** 2)
+
+
+SEGMENTS = [linear, curved, sine, sphere_increasing, sphere_decreasing]
+"""""" # Enable auto-doc for data member
+
+
+class GradientFile:
+
+ gradient = None
+
+ def getpalette(self, entries=256):
+
+ palette = []
+
+ ix = 0
+ x0, x1, xm, rgb0, rgb1, segment = self.gradient[ix]
+
+ for i in range(entries):
+
+ x = i / (entries - 1)
+
+ while x1 < x:
+ ix += 1
+ x0, x1, xm, rgb0, rgb1, segment = self.gradient[ix]
+
+ w = x1 - x0
+
+ if w < EPSILON:
+ scale = segment(0.5, 0.5)
+ else:
+ scale = segment((xm - x0) / w, (x - x0) / w)
+
+ # expand to RGBA
+ r = o8(int(255 * ((rgb1[0] - rgb0[0]) * scale + rgb0[0]) + 0.5))
+ g = o8(int(255 * ((rgb1[1] - rgb0[1]) * scale + rgb0[1]) + 0.5))
+ b = o8(int(255 * ((rgb1[2] - rgb0[2]) * scale + rgb0[2]) + 0.5))
+ a = o8(int(255 * ((rgb1[3] - rgb0[3]) * scale + rgb0[3]) + 0.5))
+
+ # add to palette
+ palette.append(r + g + b + a)
+
+ return b"".join(palette), "RGBA"
+
+
+class GimpGradientFile(GradientFile):
+ """File handler for GIMP's gradient format."""
+
+ def __init__(self, fp):
+
+ if fp.readline()[:13] != b"GIMP Gradient":
+ raise SyntaxError("not a GIMP gradient file")
+
+ line = fp.readline()
+
+ # GIMP 1.2 gradient files don't contain a name, but GIMP 1.3 files do
+ if line.startswith(b"Name: "):
+ line = fp.readline().strip()
+
+ count = int(line)
+
+ gradient = []
+
+ for i in range(count):
+
+ s = fp.readline().split()
+ w = [float(x) for x in s[:11]]
+
+ x0, x1 = w[0], w[2]
+ xm = w[1]
+ rgb0 = w[3:7]
+ rgb1 = w[7:11]
+
+ segment = SEGMENTS[int(s[11])]
+ cspace = int(s[12])
+
+ if cspace != 0:
+ raise OSError("cannot handle HSV colour space")
+
+ gradient.append((x0, x1, xm, rgb0, rgb1, segment))
+
+ self.gradient = gradient
diff --git a/venv/Lib/site-packages/PIL/GimpPaletteFile.py b/venv/Lib/site-packages/PIL/GimpPaletteFile.py
new file mode 100644
index 0000000..4d7cfba
--- /dev/null
+++ b/venv/Lib/site-packages/PIL/GimpPaletteFile.py
@@ -0,0 +1,56 @@
+#
+# Python Imaging Library
+# $Id$
+#
+# stuff to read GIMP palette files
+#
+# History:
+# 1997-08-23 fl Created
+# 2004-09-07 fl Support GIMP 2.0 palette files.
+#
+# Copyright (c) Secret Labs AB 1997-2004. All rights reserved.
+# Copyright (c) Fredrik Lundh 1997-2004.
+#
+# See the README file for information on usage and redistribution.
+#
+
+import re
+
+from ._binary import o8
+
+
+class GimpPaletteFile:
+ """File handler for GIMP's palette format."""
+
+ rawmode = "RGB"
+
+ def __init__(self, fp):
+
+ self.palette = [o8(i) * 3 for i in range(256)]
+
+ if fp.readline()[:12] != b"GIMP Palette":
+ raise SyntaxError("not a GIMP palette file")
+
+ for i in range(256):
+
+ s = fp.readline()
+ if not s:
+ break
+
+ # skip fields and comment lines
+ if re.match(rb"\w+:|#", s):
+ continue
+ if len(s) > 100:
+ raise SyntaxError("bad palette file")
+
+ v = tuple(map(int, s.split()[:3]))
+ if len(v) != 3:
+ raise ValueError("bad palette entry")
+
+ self.palette[i] = o8(v[0]) + o8(v[1]) + o8(v[2])
+
+ self.palette = b"".join(self.palette)
+
+ def getpalette(self):
+
+ return self.palette, self.rawmode
diff --git a/venv/Lib/site-packages/PIL/GribStubImagePlugin.py b/venv/Lib/site-packages/PIL/GribStubImagePlugin.py
new file mode 100644
index 0000000..cc9bc26
--- /dev/null
+++ b/venv/Lib/site-packages/PIL/GribStubImagePlugin.py
@@ -0,0 +1,73 @@
+#
+# The Python Imaging Library
+# $Id$
+#
+# GRIB stub adapter
+#
+# Copyright (c) 1996-2003 by Fredrik Lundh
+#
+# See the README file for information on usage and redistribution.
+#
+
+from . import Image, ImageFile
+
+_handler = None
+
+
+def register_handler(handler):
+ """
+ Install application-specific GRIB image handler.
+
+ :param handler: Handler object.
+ """
+ global _handler
+ _handler = handler
+
+
+# --------------------------------------------------------------------
+# Image adapter
+
+
+def _accept(prefix):
+ return prefix[0:4] == b"GRIB" and prefix[7] == 1
+
+
+class GribStubImageFile(ImageFile.StubImageFile):
+
+ format = "GRIB"
+ format_description = "GRIB"
+
+ def _open(self):
+
+ offset = self.fp.tell()
+
+ if not _accept(self.fp.read(8)):
+ raise SyntaxError("Not a GRIB file")
+
+ self.fp.seek(offset)
+
+ # make something up
+ self.mode = "F"
+ self._size = 1, 1
+
+ loader = self._load()
+ if loader:
+ loader.open(self)
+
+ def _load(self):
+ return _handler
+
+
+def _save(im, fp, filename):
+ if _handler is None or not hasattr(_handler, "save"):
+ raise OSError("GRIB save handler not installed")
+ _handler.save(im, fp, filename)
+
+
+# --------------------------------------------------------------------
+# Registry
+
+Image.register_open(GribStubImageFile.format, GribStubImageFile, _accept)
+Image.register_save(GribStubImageFile.format, _save)
+
+Image.register_extension(GribStubImageFile.format, ".grib")
diff --git a/venv/Lib/site-packages/PIL/Hdf5StubImagePlugin.py b/venv/Lib/site-packages/PIL/Hdf5StubImagePlugin.py
new file mode 100644
index 0000000..df11cf2
--- /dev/null
+++ b/venv/Lib/site-packages/PIL/Hdf5StubImagePlugin.py
@@ -0,0 +1,73 @@
+#
+# The Python Imaging Library
+# $Id$
+#
+# HDF5 stub adapter
+#
+# Copyright (c) 2000-2003 by Fredrik Lundh
+#
+# See the README file for information on usage and redistribution.
+#
+
+from . import Image, ImageFile
+
+_handler = None
+
+
+def register_handler(handler):
+ """
+ Install application-specific HDF5 image handler.
+
+ :param handler: Handler object.
+ """
+ global _handler
+ _handler = handler
+
+
+# --------------------------------------------------------------------
+# Image adapter
+
+
+def _accept(prefix):
+ return prefix[:8] == b"\x89HDF\r\n\x1a\n"
+
+
+class HDF5StubImageFile(ImageFile.StubImageFile):
+
+ format = "HDF5"
+ format_description = "HDF5"
+
+ def _open(self):
+
+ offset = self.fp.tell()
+
+ if not _accept(self.fp.read(8)):
+ raise SyntaxError("Not an HDF file")
+
+ self.fp.seek(offset)
+
+ # make something up
+ self.mode = "F"
+ self._size = 1, 1
+
+ loader = self._load()
+ if loader:
+ loader.open(self)
+
+ def _load(self):
+ return _handler
+
+
+def _save(im, fp, filename):
+ if _handler is None or not hasattr(_handler, "save"):
+ raise OSError("HDF5 save handler not installed")
+ _handler.save(im, fp, filename)
+
+
+# --------------------------------------------------------------------
+# Registry
+
+Image.register_open(HDF5StubImageFile.format, HDF5StubImageFile, _accept)
+Image.register_save(HDF5StubImageFile.format, _save)
+
+Image.register_extensions(HDF5StubImageFile.format, [".h5", ".hdf"])
diff --git a/venv/Lib/site-packages/PIL/IcnsImagePlugin.py b/venv/Lib/site-packages/PIL/IcnsImagePlugin.py
new file mode 100644
index 0000000..fa192f0
--- /dev/null
+++ b/venv/Lib/site-packages/PIL/IcnsImagePlugin.py
@@ -0,0 +1,392 @@
+#
+# The Python Imaging Library.
+# $Id$
+#
+# macOS icns file decoder, based on icns.py by Bob Ippolito.
+#
+# history:
+# 2004-10-09 fl Turned into a PIL plugin; removed 2.3 dependencies.
+# 2020-04-04 Allow saving on all operating systems.
+#
+# Copyright (c) 2004 by Bob Ippolito.
+# Copyright (c) 2004 by Secret Labs.
+# Copyright (c) 2004 by Fredrik Lundh.
+# Copyright (c) 2014 by Alastair Houghton.
+# Copyright (c) 2020 by Pan Jing.
+#
+# See the README file for information on usage and redistribution.
+#
+
+import io
+import os
+import struct
+import sys
+
+from PIL import Image, ImageFile, PngImagePlugin, features
+
+enable_jpeg2k = features.check_codec("jpg_2000")
+if enable_jpeg2k:
+ from PIL import Jpeg2KImagePlugin
+
+MAGIC = b"icns"
+HEADERSIZE = 8
+
+
+def nextheader(fobj):
+ return struct.unpack(">4sI", fobj.read(HEADERSIZE))
+
+
+def read_32t(fobj, start_length, size):
+ # The 128x128 icon seems to have an extra header for some reason.
+ (start, length) = start_length
+ fobj.seek(start)
+ sig = fobj.read(4)
+ if sig != b"\x00\x00\x00\x00":
+ raise SyntaxError("Unknown signature, expecting 0x00000000")
+ return read_32(fobj, (start + 4, length - 4), size)
+
+
+def read_32(fobj, start_length, size):
+ """
+ Read a 32bit RGB icon resource. Seems to be either uncompressed or
+ an RLE packbits-like scheme.
+ """
+ (start, length) = start_length
+ fobj.seek(start)
+ pixel_size = (size[0] * size[2], size[1] * size[2])
+ sizesq = pixel_size[0] * pixel_size[1]
+ if length == sizesq * 3:
+ # uncompressed ("RGBRGBGB")
+ indata = fobj.read(length)
+ im = Image.frombuffer("RGB", pixel_size, indata, "raw", "RGB", 0, 1)
+ else:
+ # decode image
+ im = Image.new("RGB", pixel_size, None)
+ for band_ix in range(3):
+ data = []
+ bytesleft = sizesq
+ while bytesleft > 0:
+ byte = fobj.read(1)
+ if not byte:
+ break
+ byte = byte[0]
+ if byte & 0x80:
+ blocksize = byte - 125
+ byte = fobj.read(1)
+ for i in range(blocksize):
+ data.append(byte)
+ else:
+ blocksize = byte + 1
+ data.append(fobj.read(blocksize))
+ bytesleft -= blocksize
+ if bytesleft <= 0:
+ break
+ if bytesleft != 0:
+ raise SyntaxError(f"Error reading channel [{repr(bytesleft)} left]")
+ band = Image.frombuffer("L", pixel_size, b"".join(data), "raw", "L", 0, 1)
+ im.im.putband(band.im, band_ix)
+ return {"RGB": im}
+
+
+def read_mk(fobj, start_length, size):
+ # Alpha masks seem to be uncompressed
+ start = start_length[0]
+ fobj.seek(start)
+ pixel_size = (size[0] * size[2], size[1] * size[2])
+ sizesq = pixel_size[0] * pixel_size[1]
+ band = Image.frombuffer("L", pixel_size, fobj.read(sizesq), "raw", "L", 0, 1)
+ return {"A": band}
+
+
+def read_png_or_jpeg2000(fobj, start_length, size):
+ (start, length) = start_length
+ fobj.seek(start)
+ sig = fobj.read(12)
+ if sig[:8] == b"\x89PNG\x0d\x0a\x1a\x0a":
+ fobj.seek(start)
+ im = PngImagePlugin.PngImageFile(fobj)
+ Image._decompression_bomb_check(im.size)
+ return {"RGBA": im}
+ elif (
+ sig[:4] == b"\xff\x4f\xff\x51"
+ or sig[:4] == b"\x0d\x0a\x87\x0a"
+ or sig == b"\x00\x00\x00\x0cjP \x0d\x0a\x87\x0a"
+ ):
+ if not enable_jpeg2k:
+ raise ValueError(
+ "Unsupported icon subimage format (rebuild PIL "
+ "with JPEG 2000 support to fix this)"
+ )
+ # j2k, jpc or j2c
+ fobj.seek(start)
+ jp2kstream = fobj.read(length)
+ f = io.BytesIO(jp2kstream)
+ im = Jpeg2KImagePlugin.Jpeg2KImageFile(f)
+ Image._decompression_bomb_check(im.size)
+ if im.mode != "RGBA":
+ im = im.convert("RGBA")
+ return {"RGBA": im}
+ else:
+ raise ValueError("Unsupported icon subimage format")
+
+
+class IcnsFile:
+
+ SIZES = {
+ (512, 512, 2): [(b"ic10", read_png_or_jpeg2000)],
+ (512, 512, 1): [(b"ic09", read_png_or_jpeg2000)],
+ (256, 256, 2): [(b"ic14", read_png_or_jpeg2000)],
+ (256, 256, 1): [(b"ic08", read_png_or_jpeg2000)],
+ (128, 128, 2): [(b"ic13", read_png_or_jpeg2000)],
+ (128, 128, 1): [
+ (b"ic07", read_png_or_jpeg2000),
+ (b"it32", read_32t),
+ (b"t8mk", read_mk),
+ ],
+ (64, 64, 1): [(b"icp6", read_png_or_jpeg2000)],
+ (32, 32, 2): [(b"ic12", read_png_or_jpeg2000)],
+ (48, 48, 1): [(b"ih32", read_32), (b"h8mk", read_mk)],
+ (32, 32, 1): [
+ (b"icp5", read_png_or_jpeg2000),
+ (b"il32", read_32),
+ (b"l8mk", read_mk),
+ ],
+ (16, 16, 2): [(b"ic11", read_png_or_jpeg2000)],
+ (16, 16, 1): [
+ (b"icp4", read_png_or_jpeg2000),
+ (b"is32", read_32),
+ (b"s8mk", read_mk),
+ ],
+ }
+
+ def __init__(self, fobj):
+ """
+ fobj is a file-like object as an icns resource
+ """
+ # signature : (start, length)
+ self.dct = dct = {}
+ self.fobj = fobj
+ sig, filesize = nextheader(fobj)
+ if not _accept(sig):
+ raise SyntaxError("not an icns file")
+ i = HEADERSIZE
+ while i < filesize:
+ sig, blocksize = nextheader(fobj)
+ if blocksize <= 0:
+ raise SyntaxError("invalid block header")
+ i += HEADERSIZE
+ blocksize -= HEADERSIZE
+ dct[sig] = (i, blocksize)
+ fobj.seek(blocksize, io.SEEK_CUR)
+ i += blocksize
+
+ def itersizes(self):
+ sizes = []
+ for size, fmts in self.SIZES.items():
+ for (fmt, reader) in fmts:
+ if fmt in self.dct:
+ sizes.append(size)
+ break
+ return sizes
+
+ def bestsize(self):
+ sizes = self.itersizes()
+ if not sizes:
+ raise SyntaxError("No 32bit icon resources found")
+ return max(sizes)
+
+ def dataforsize(self, size):
+ """
+ Get an icon resource as {channel: array}. Note that
+ the arrays are bottom-up like windows bitmaps and will likely
+ need to be flipped or transposed in some way.
+ """
+ dct = {}
+ for code, reader in self.SIZES[size]:
+ desc = self.dct.get(code)
+ if desc is not None:
+ dct.update(reader(self.fobj, desc, size))
+ return dct
+
+ def getimage(self, size=None):
+ if size is None:
+ size = self.bestsize()
+ if len(size) == 2:
+ size = (size[0], size[1], 1)
+ channels = self.dataforsize(size)
+
+ im = channels.get("RGBA", None)
+ if im:
+ return im
+
+ im = channels.get("RGB").copy()
+ try:
+ im.putalpha(channels["A"])
+ except KeyError:
+ pass
+ return im
+
+
+##
+# Image plugin for Mac OS icons.
+
+
+class IcnsImageFile(ImageFile.ImageFile):
+ """
+ PIL image support for Mac OS .icns files.
+ Chooses the best resolution, but will possibly load
+ a different size image if you mutate the size attribute
+ before calling 'load'.
+
+ The info dictionary has a key 'sizes' that is a list
+ of sizes that the icns file has.
+ """
+
+ format = "ICNS"
+ format_description = "Mac OS icns resource"
+
+ def _open(self):
+ self.icns = IcnsFile(self.fp)
+ self.mode = "RGBA"
+ self.info["sizes"] = self.icns.itersizes()
+ self.best_size = self.icns.bestsize()
+ self.size = (
+ self.best_size[0] * self.best_size[2],
+ self.best_size[1] * self.best_size[2],
+ )
+
+ @property
+ def size(self):
+ return self._size
+
+ @size.setter
+ def size(self, value):
+ info_size = value
+ if info_size not in self.info["sizes"] and len(info_size) == 2:
+ info_size = (info_size[0], info_size[1], 1)
+ if (
+ info_size not in self.info["sizes"]
+ and len(info_size) == 3
+ and info_size[2] == 1
+ ):
+ simple_sizes = [
+ (size[0] * size[2], size[1] * size[2]) for size in self.info["sizes"]
+ ]
+ if value in simple_sizes:
+ info_size = self.info["sizes"][simple_sizes.index(value)]
+ if info_size not in self.info["sizes"]:
+ raise ValueError("This is not one of the allowed sizes of this image")
+ self._size = value
+
+ def load(self):
+ if len(self.size) == 3:
+ self.best_size = self.size
+ self.size = (
+ self.best_size[0] * self.best_size[2],
+ self.best_size[1] * self.best_size[2],
+ )
+
+ px = Image.Image.load(self)
+ if self.im is not None and self.im.size == self.size:
+ # Already loaded
+ return px
+ self.load_prepare()
+ # This is likely NOT the best way to do it, but whatever.
+ im = self.icns.getimage(self.best_size)
+
+ # If this is a PNG or JPEG 2000, it won't be loaded yet
+ px = im.load()
+
+ self.im = im.im
+ self.mode = im.mode
+ self.size = im.size
+
+ return px
+
+
+def _save(im, fp, filename):
+ """
+ Saves the image as a series of PNG files,
+ that are then combined into a .icns file.
+ """
+ if hasattr(fp, "flush"):
+ fp.flush()
+
+ sizes = {
+ b"ic07": 128,
+ b"ic08": 256,
+ b"ic09": 512,
+ b"ic10": 1024,
+ b"ic11": 32,
+ b"ic12": 64,
+ b"ic13": 256,
+ b"ic14": 512,
+ }
+ provided_images = {im.width: im for im in im.encoderinfo.get("append_images", [])}
+ size_streams = {}
+ for size in set(sizes.values()):
+ image = (
+ provided_images[size]
+ if size in provided_images
+ else im.resize((size, size))
+ )
+
+ temp = io.BytesIO()
+ image.save(temp, "png")
+ size_streams[size] = temp.getvalue()
+
+ entries = []
+ for type, size in sizes.items():
+ stream = size_streams[size]
+ entries.append(
+ {"type": type, "size": HEADERSIZE + len(stream), "stream": stream}
+ )
+
+ # Header
+ fp.write(MAGIC)
+ file_length = HEADERSIZE # Header
+ file_length += HEADERSIZE + 8 * len(entries) # TOC
+ file_length += sum(entry["size"] for entry in entries)
+ fp.write(struct.pack(">i", file_length))
+
+ # TOC
+ fp.write(b"TOC ")
+ fp.write(struct.pack(">i", HEADERSIZE + len(entries) * HEADERSIZE))
+ for entry in entries:
+ fp.write(entry["type"])
+ fp.write(struct.pack(">i", entry["size"]))
+
+ # Data
+ for entry in entries:
+ fp.write(entry["type"])
+ fp.write(struct.pack(">i", entry["size"]))
+ fp.write(entry["stream"])
+
+ if hasattr(fp, "flush"):
+ fp.flush()
+
+
+def _accept(prefix):
+ return prefix[:4] == MAGIC
+
+
+Image.register_open(IcnsImageFile.format, IcnsImageFile, _accept)
+Image.register_extension(IcnsImageFile.format, ".icns")
+
+Image.register_save(IcnsImageFile.format, _save)
+Image.register_mime(IcnsImageFile.format, "image/icns")
+
+if __name__ == "__main__":
+ if len(sys.argv) < 2:
+ print("Syntax: python3 IcnsImagePlugin.py [file]")
+ sys.exit()
+
+ with open(sys.argv[1], "rb") as fp:
+ imf = IcnsImageFile(fp)
+ for size in imf.info["sizes"]:
+ imf.size = size
+ imf.save("out-%s-%s-%s.png" % size)
+ with Image.open(sys.argv[1]) as im:
+ im.save("out.png")
+ if sys.platform == "windows":
+ os.startfile("out.png")
diff --git a/venv/Lib/site-packages/PIL/IcoImagePlugin.py b/venv/Lib/site-packages/PIL/IcoImagePlugin.py
new file mode 100644
index 0000000..17b9855
--- /dev/null
+++ b/venv/Lib/site-packages/PIL/IcoImagePlugin.py
@@ -0,0 +1,355 @@
+#
+# The Python Imaging Library.
+# $Id$
+#
+# Windows Icon support for PIL
+#
+# History:
+# 96-05-27 fl Created
+#
+# Copyright (c) Secret Labs AB 1997.
+# Copyright (c) Fredrik Lundh 1996.
+#
+# See the README file for information on usage and redistribution.
+#
+
+# This plugin is a refactored version of Win32IconImagePlugin by Bryan Davis
+# .
+# https://code.google.com/archive/p/casadebender/wikis/Win32IconImagePlugin.wiki
+#
+# Icon format references:
+# * https://en.wikipedia.org/wiki/ICO_(file_format)
+# * https://msdn.microsoft.com/en-us/library/ms997538.aspx
+
+
+import warnings
+from io import BytesIO
+from math import ceil, log
+
+from . import BmpImagePlugin, Image, ImageFile, PngImagePlugin
+from ._binary import i16le as i16
+from ._binary import i32le as i32
+from ._binary import o8
+from ._binary import o16le as o16
+from ._binary import o32le as o32
+
+#
+# --------------------------------------------------------------------
+
+_MAGIC = b"\0\0\1\0"
+
+
+def _save(im, fp, filename):
+ fp.write(_MAGIC) # (2+2)
+ bmp = im.encoderinfo.get("bitmap_format") == "bmp"
+ sizes = im.encoderinfo.get(
+ "sizes",
+ [(16, 16), (24, 24), (32, 32), (48, 48), (64, 64), (128, 128), (256, 256)],
+ )
+ frames = []
+ provided_ims = [im] + im.encoderinfo.get("append_images", [])
+ width, height = im.size
+ for size in sorted(set(sizes)):
+ if size[0] > width or size[1] > height or size[0] > 256 or size[1] > 256:
+ continue
+
+ for provided_im in provided_ims:
+ if provided_im.size != size:
+ continue
+ frames.append(provided_im)
+ if bmp:
+ bits = BmpImagePlugin.SAVE[provided_im.mode][1]
+ bits_used = [bits]
+ for other_im in provided_ims:
+ if other_im.size != size:
+ continue
+ bits = BmpImagePlugin.SAVE[other_im.mode][1]
+ if bits not in bits_used:
+ # Another image has been supplied for this size
+ # with a different bit depth
+ frames.append(other_im)
+ bits_used.append(bits)
+ break
+ else:
+ # TODO: invent a more convenient method for proportional scalings
+ frame = provided_im.copy()
+ frame.thumbnail(size, Image.Resampling.LANCZOS, reducing_gap=None)
+ frames.append(frame)
+ fp.write(o16(len(frames))) # idCount(2)
+ offset = fp.tell() + len(frames) * 16
+ for frame in frames:
+ width, height = frame.size
+ # 0 means 256
+ fp.write(o8(width if width < 256 else 0)) # bWidth(1)
+ fp.write(o8(height if height < 256 else 0)) # bHeight(1)
+
+ bits, colors = BmpImagePlugin.SAVE[frame.mode][1:] if bmp else (32, 0)
+ fp.write(o8(colors)) # bColorCount(1)
+ fp.write(b"\0") # bReserved(1)
+ fp.write(b"\0\0") # wPlanes(2)
+ fp.write(o16(bits)) # wBitCount(2)
+
+ image_io = BytesIO()
+ if bmp:
+ frame.save(image_io, "dib")
+
+ if bits != 32:
+ and_mask = Image.new("1", size)
+ ImageFile._save(
+ and_mask, image_io, [("raw", (0, 0) + size, 0, ("1", 0, -1))]
+ )
+ else:
+ frame.save(image_io, "png")
+ image_io.seek(0)
+ image_bytes = image_io.read()
+ if bmp:
+ image_bytes = image_bytes[:8] + o32(height * 2) + image_bytes[12:]
+ bytes_len = len(image_bytes)
+ fp.write(o32(bytes_len)) # dwBytesInRes(4)
+ fp.write(o32(offset)) # dwImageOffset(4)
+ current = fp.tell()
+ fp.seek(offset)
+ fp.write(image_bytes)
+ offset = offset + bytes_len
+ fp.seek(current)
+
+
+def _accept(prefix):
+ return prefix[:4] == _MAGIC
+
+
+class IcoFile:
+ def __init__(self, buf):
+ """
+ Parse image from file-like object containing ico file data
+ """
+
+ # check magic
+ s = buf.read(6)
+ if not _accept(s):
+ raise SyntaxError("not an ICO file")
+
+ self.buf = buf
+ self.entry = []
+
+ # Number of items in file
+ self.nb_items = i16(s, 4)
+
+ # Get headers for each item
+ for i in range(self.nb_items):
+ s = buf.read(16)
+
+ icon_header = {
+ "width": s[0],
+ "height": s[1],
+ "nb_color": s[2], # No. of colors in image (0 if >=8bpp)
+ "reserved": s[3],
+ "planes": i16(s, 4),
+ "bpp": i16(s, 6),
+ "size": i32(s, 8),
+ "offset": i32(s, 12),
+ }
+
+ # See Wikipedia
+ for j in ("width", "height"):
+ if not icon_header[j]:
+ icon_header[j] = 256
+
+ # See Wikipedia notes about color depth.
+ # We need this just to differ images with equal sizes
+ icon_header["color_depth"] = (
+ icon_header["bpp"]
+ or (
+ icon_header["nb_color"] != 0
+ and ceil(log(icon_header["nb_color"], 2))
+ )
+ or 256
+ )
+
+ icon_header["dim"] = (icon_header["width"], icon_header["height"])
+ icon_header["square"] = icon_header["width"] * icon_header["height"]
+
+ self.entry.append(icon_header)
+
+ self.entry = sorted(self.entry, key=lambda x: x["color_depth"])
+ # ICO images are usually squares
+ # self.entry = sorted(self.entry, key=lambda x: x['width'])
+ self.entry = sorted(self.entry, key=lambda x: x["square"])
+ self.entry.reverse()
+
+ def sizes(self):
+ """
+ Get a list of all available icon sizes and color depths.
+ """
+ return {(h["width"], h["height"]) for h in self.entry}
+
+ def getentryindex(self, size, bpp=False):
+ for (i, h) in enumerate(self.entry):
+ if size == h["dim"] and (bpp is False or bpp == h["color_depth"]):
+ return i
+ return 0
+
+ def getimage(self, size, bpp=False):
+ """
+ Get an image from the icon
+ """
+ return self.frame(self.getentryindex(size, bpp))
+
+ def frame(self, idx):
+ """
+ Get an image from frame idx
+ """
+
+ header = self.entry[idx]
+
+ self.buf.seek(header["offset"])
+ data = self.buf.read(8)
+ self.buf.seek(header["offset"])
+
+ if data[:8] == PngImagePlugin._MAGIC:
+ # png frame
+ im = PngImagePlugin.PngImageFile(self.buf)
+ Image._decompression_bomb_check(im.size)
+ else:
+ # XOR + AND mask bmp frame
+ im = BmpImagePlugin.DibImageFile(self.buf)
+ Image._decompression_bomb_check(im.size)
+
+ # change tile dimension to only encompass XOR image
+ im._size = (im.size[0], int(im.size[1] / 2))
+ d, e, o, a = im.tile[0]
+ im.tile[0] = d, (0, 0) + im.size, o, a
+
+ # figure out where AND mask image starts
+ bpp = header["bpp"]
+ if 32 == bpp:
+ # 32-bit color depth icon image allows semitransparent areas
+ # PIL's DIB format ignores transparency bits, recover them.
+ # The DIB is packed in BGRX byte order where X is the alpha
+ # channel.
+
+ # Back up to start of bmp data
+ self.buf.seek(o)
+ # extract every 4th byte (eg. 3,7,11,15,...)
+ alpha_bytes = self.buf.read(im.size[0] * im.size[1] * 4)[3::4]
+
+ # convert to an 8bpp grayscale image
+ mask = Image.frombuffer(
+ "L", # 8bpp
+ im.size, # (w, h)
+ alpha_bytes, # source chars
+ "raw", # raw decoder
+ ("L", 0, -1), # 8bpp inverted, unpadded, reversed
+ )
+ else:
+ # get AND image from end of bitmap
+ w = im.size[0]
+ if (w % 32) > 0:
+ # bitmap row data is aligned to word boundaries
+ w += 32 - (im.size[0] % 32)
+
+ # the total mask data is
+ # padded row size * height / bits per char
+
+ total_bytes = int((w * im.size[1]) / 8)
+ and_mask_offset = header["offset"] + header["size"] - total_bytes
+
+ self.buf.seek(and_mask_offset)
+ mask_data = self.buf.read(total_bytes)
+
+ # convert raw data to image
+ mask = Image.frombuffer(
+ "1", # 1 bpp
+ im.size, # (w, h)
+ mask_data, # source chars
+ "raw", # raw decoder
+ ("1;I", int(w / 8), -1), # 1bpp inverted, padded, reversed
+ )
+
+ # now we have two images, im is XOR image and mask is AND image
+
+ # apply mask image as alpha channel
+ im = im.convert("RGBA")
+ im.putalpha(mask)
+
+ return im
+
+
+##
+# Image plugin for Windows Icon files.
+
+
+class IcoImageFile(ImageFile.ImageFile):
+ """
+ PIL read-only image support for Microsoft Windows .ico files.
+
+ By default the largest resolution image in the file will be loaded. This
+ can be changed by altering the 'size' attribute before calling 'load'.
+
+ The info dictionary has a key 'sizes' that is a list of the sizes available
+ in the icon file.
+
+ Handles classic, XP and Vista icon formats.
+
+ When saving, PNG compression is used. Support for this was only added in
+ Windows Vista. If you are unable to view the icon in Windows, convert the
+ image to "RGBA" mode before saving.
+
+ This plugin is a refactored version of Win32IconImagePlugin by Bryan Davis
+ .
+ https://code.google.com/archive/p/casadebender/wikis/Win32IconImagePlugin.wiki
+ """
+
+ format = "ICO"
+ format_description = "Windows Icon"
+
+ def _open(self):
+ self.ico = IcoFile(self.fp)
+ self.info["sizes"] = self.ico.sizes()
+ self.size = self.ico.entry[0]["dim"]
+ self.load()
+
+ @property
+ def size(self):
+ return self._size
+
+ @size.setter
+ def size(self, value):
+ if value not in self.info["sizes"]:
+ raise ValueError("This is not one of the allowed sizes of this image")
+ self._size = value
+
+ def load(self):
+ if self.im is not None and self.im.size == self.size:
+ # Already loaded
+ return Image.Image.load(self)
+ im = self.ico.getimage(self.size)
+ # if tile is PNG, it won't really be loaded yet
+ im.load()
+ self.im = im.im
+ self.mode = im.mode
+ if im.size != self.size:
+ warnings.warn("Image was not the expected size")
+
+ index = self.ico.getentryindex(self.size)
+ sizes = list(self.info["sizes"])
+ sizes[index] = im.size
+ self.info["sizes"] = set(sizes)
+
+ self.size = im.size
+
+ def load_seek(self):
+ # Flag the ImageFile.Parser so that it
+ # just does all the decode at the end.
+ pass
+
+
+#
+# --------------------------------------------------------------------
+
+
+Image.register_open(IcoImageFile.format, IcoImageFile, _accept)
+Image.register_save(IcoImageFile.format, _save)
+Image.register_extension(IcoImageFile.format, ".ico")
+
+Image.register_mime(IcoImageFile.format, "image/x-icon")
diff --git a/venv/Lib/site-packages/PIL/ImImagePlugin.py b/venv/Lib/site-packages/PIL/ImImagePlugin.py
new file mode 100644
index 0000000..f7e690b
--- /dev/null
+++ b/venv/Lib/site-packages/PIL/ImImagePlugin.py
@@ -0,0 +1,376 @@
+#
+# The Python Imaging Library.
+# $Id$
+#
+# IFUNC IM file handling for PIL
+#
+# history:
+# 1995-09-01 fl Created.
+# 1997-01-03 fl Save palette images
+# 1997-01-08 fl Added sequence support
+# 1997-01-23 fl Added P and RGB save support
+# 1997-05-31 fl Read floating point images
+# 1997-06-22 fl Save floating point images
+# 1997-08-27 fl Read and save 1-bit images
+# 1998-06-25 fl Added support for RGB+LUT images
+# 1998-07-02 fl Added support for YCC images
+# 1998-07-15 fl Renamed offset attribute to avoid name clash
+# 1998-12-29 fl Added I;16 support
+# 2001-02-17 fl Use 're' instead of 'regex' (Python 2.1) (0.7)
+# 2003-09-26 fl Added LA/PA support
+#
+# Copyright (c) 1997-2003 by Secret Labs AB.
+# Copyright (c) 1995-2001 by Fredrik Lundh.
+#
+# See the README file for information on usage and redistribution.
+#
+
+
+import os
+import re
+
+from . import Image, ImageFile, ImagePalette
+
+# --------------------------------------------------------------------
+# Standard tags
+
+COMMENT = "Comment"
+DATE = "Date"
+EQUIPMENT = "Digitalization equipment"
+FRAMES = "File size (no of images)"
+LUT = "Lut"
+NAME = "Name"
+SCALE = "Scale (x,y)"
+SIZE = "Image size (x*y)"
+MODE = "Image type"
+
+TAGS = {
+ COMMENT: 0,
+ DATE: 0,
+ EQUIPMENT: 0,
+ FRAMES: 0,
+ LUT: 0,
+ NAME: 0,
+ SCALE: 0,
+ SIZE: 0,
+ MODE: 0,
+}
+
+OPEN = {
+ # ifunc93/p3cfunc formats
+ "0 1 image": ("1", "1"),
+ "L 1 image": ("1", "1"),
+ "Greyscale image": ("L", "L"),
+ "Grayscale image": ("L", "L"),
+ "RGB image": ("RGB", "RGB;L"),
+ "RLB image": ("RGB", "RLB"),
+ "RYB image": ("RGB", "RLB"),
+ "B1 image": ("1", "1"),
+ "B2 image": ("P", "P;2"),
+ "B4 image": ("P", "P;4"),
+ "X 24 image": ("RGB", "RGB"),
+ "L 32 S image": ("I", "I;32"),
+ "L 32 F image": ("F", "F;32"),
+ # old p3cfunc formats
+ "RGB3 image": ("RGB", "RGB;T"),
+ "RYB3 image": ("RGB", "RYB;T"),
+ # extensions
+ "LA image": ("LA", "LA;L"),
+ "PA image": ("LA", "PA;L"),
+ "RGBA image": ("RGBA", "RGBA;L"),
+ "RGBX image": ("RGBX", "RGBX;L"),
+ "CMYK image": ("CMYK", "CMYK;L"),
+ "YCC image": ("YCbCr", "YCbCr;L"),
+}
+
+# ifunc95 extensions
+for i in ["8", "8S", "16", "16S", "32", "32F"]:
+ OPEN[f"L {i} image"] = ("F", f"F;{i}")
+ OPEN[f"L*{i} image"] = ("F", f"F;{i}")
+for i in ["16", "16L", "16B"]:
+ OPEN[f"L {i} image"] = (f"I;{i}", f"I;{i}")
+ OPEN[f"L*{i} image"] = (f"I;{i}", f"I;{i}")
+for i in ["32S"]:
+ OPEN[f"L {i} image"] = ("I", f"I;{i}")
+ OPEN[f"L*{i} image"] = ("I", f"I;{i}")
+for i in range(2, 33):
+ OPEN[f"L*{i} image"] = ("F", f"F;{i}")
+
+
+# --------------------------------------------------------------------
+# Read IM directory
+
+split = re.compile(rb"^([A-Za-z][^:]*):[ \t]*(.*)[ \t]*$")
+
+
+def number(s):
+ try:
+ return int(s)
+ except ValueError:
+ return float(s)
+
+
+##
+# Image plugin for the IFUNC IM file format.
+
+
+class ImImageFile(ImageFile.ImageFile):
+
+ format = "IM"
+ format_description = "IFUNC Image Memory"
+ _close_exclusive_fp_after_loading = False
+
+ def _open(self):
+
+ # Quick rejection: if there's not an LF among the first
+ # 100 bytes, this is (probably) not a text header.
+
+ if b"\n" not in self.fp.read(100):
+ raise SyntaxError("not an IM file")
+ self.fp.seek(0)
+
+ n = 0
+
+ # Default values
+ self.info[MODE] = "L"
+ self.info[SIZE] = (512, 512)
+ self.info[FRAMES] = 1
+
+ self.rawmode = "L"
+
+ while True:
+
+ s = self.fp.read(1)
+
+ # Some versions of IFUNC uses \n\r instead of \r\n...
+ if s == b"\r":
+ continue
+
+ if not s or s == b"\0" or s == b"\x1A":
+ break
+
+ # FIXME: this may read whole file if not a text file
+ s = s + self.fp.readline()
+
+ if len(s) > 100:
+ raise SyntaxError("not an IM file")
+
+ if s[-2:] == b"\r\n":
+ s = s[:-2]
+ elif s[-1:] == b"\n":
+ s = s[:-1]
+
+ try:
+ m = split.match(s)
+ except re.error as e:
+ raise SyntaxError("not an IM file") from e
+
+ if m:
+
+ k, v = m.group(1, 2)
+
+ # Don't know if this is the correct encoding,
+ # but a decent guess (I guess)
+ k = k.decode("latin-1", "replace")
+ v = v.decode("latin-1", "replace")
+
+ # Convert value as appropriate
+ if k in [FRAMES, SCALE, SIZE]:
+ v = v.replace("*", ",")
+ v = tuple(map(number, v.split(",")))
+ if len(v) == 1:
+ v = v[0]
+ elif k == MODE and v in OPEN:
+ v, self.rawmode = OPEN[v]
+
+ # Add to dictionary. Note that COMMENT tags are
+ # combined into a list of strings.
+ if k == COMMENT:
+ if k in self.info:
+ self.info[k].append(v)
+ else:
+ self.info[k] = [v]
+ else:
+ self.info[k] = v
+
+ if k in TAGS:
+ n += 1
+
+ else:
+
+ raise SyntaxError(
+ "Syntax error in IM header: " + s.decode("ascii", "replace")
+ )
+
+ if not n:
+ raise SyntaxError("Not an IM file")
+
+ # Basic attributes
+ self._size = self.info[SIZE]
+ self.mode = self.info[MODE]
+
+ # Skip forward to start of image data
+ while s and s[0:1] != b"\x1A":
+ s = self.fp.read(1)
+ if not s:
+ raise SyntaxError("File truncated")
+
+ if LUT in self.info:
+ # convert lookup table to palette or lut attribute
+ palette = self.fp.read(768)
+ greyscale = 1 # greyscale palette
+ linear = 1 # linear greyscale palette
+ for i in range(256):
+ if palette[i] == palette[i + 256] == palette[i + 512]:
+ if palette[i] != i:
+ linear = 0
+ else:
+ greyscale = 0
+ if self.mode in ["L", "LA", "P", "PA"]:
+ if greyscale:
+ if not linear:
+ self.lut = list(palette[:256])
+ else:
+ if self.mode in ["L", "P"]:
+ self.mode = self.rawmode = "P"
+ elif self.mode in ["LA", "PA"]:
+ self.mode = "PA"
+ self.rawmode = "PA;L"
+ self.palette = ImagePalette.raw("RGB;L", palette)
+ elif self.mode == "RGB":
+ if not greyscale or not linear:
+ self.lut = list(palette)
+
+ self.frame = 0
+
+ self.__offset = offs = self.fp.tell()
+
+ self.__fp = self.fp # FIXME: hack
+
+ if self.rawmode[:2] == "F;":
+
+ # ifunc95 formats
+ try:
+ # use bit decoder (if necessary)
+ bits = int(self.rawmode[2:])
+ if bits not in [8, 16, 32]:
+ self.tile = [("bit", (0, 0) + self.size, offs, (bits, 8, 3, 0, -1))]
+ return
+ except ValueError:
+ pass
+
+ if self.rawmode in ["RGB;T", "RYB;T"]:
+ # Old LabEye/3PC files. Would be very surprised if anyone
+ # ever stumbled upon such a file ;-)
+ size = self.size[0] * self.size[1]
+ self.tile = [
+ ("raw", (0, 0) + self.size, offs, ("G", 0, -1)),
+ ("raw", (0, 0) + self.size, offs + size, ("R", 0, -1)),
+ ("raw", (0, 0) + self.size, offs + 2 * size, ("B", 0, -1)),
+ ]
+ else:
+ # LabEye/IFUNC files
+ self.tile = [("raw", (0, 0) + self.size, offs, (self.rawmode, 0, -1))]
+
+ @property
+ def n_frames(self):
+ return self.info[FRAMES]
+
+ @property
+ def is_animated(self):
+ return self.info[FRAMES] > 1
+
+ def seek(self, frame):
+ if not self._seek_check(frame):
+ return
+
+ self.frame = frame
+
+ if self.mode == "1":
+ bits = 1
+ else:
+ bits = 8 * len(self.mode)
+
+ size = ((self.size[0] * bits + 7) // 8) * self.size[1]
+ offs = self.__offset + frame * size
+
+ self.fp = self.__fp
+
+ self.tile = [("raw", (0, 0) + self.size, offs, (self.rawmode, 0, -1))]
+
+ def tell(self):
+ return self.frame
+
+ def _close__fp(self):
+ try:
+ if self.__fp != self.fp:
+ self.__fp.close()
+ except AttributeError:
+ pass
+ finally:
+ self.__fp = None
+
+
+#
+# --------------------------------------------------------------------
+# Save IM files
+
+
+SAVE = {
+ # mode: (im type, raw mode)
+ "1": ("0 1", "1"),
+ "L": ("Greyscale", "L"),
+ "LA": ("LA", "LA;L"),
+ "P": ("Greyscale", "P"),
+ "PA": ("LA", "PA;L"),
+ "I": ("L 32S", "I;32S"),
+ "I;16": ("L 16", "I;16"),
+ "I;16L": ("L 16L", "I;16L"),
+ "I;16B": ("L 16B", "I;16B"),
+ "F": ("L 32F", "F;32F"),
+ "RGB": ("RGB", "RGB;L"),
+ "RGBA": ("RGBA", "RGBA;L"),
+ "RGBX": ("RGBX", "RGBX;L"),
+ "CMYK": ("CMYK", "CMYK;L"),
+ "YCbCr": ("YCC", "YCbCr;L"),
+}
+
+
+def _save(im, fp, filename):
+
+ try:
+ image_type, rawmode = SAVE[im.mode]
+ except KeyError as e:
+ raise ValueError(f"Cannot save {im.mode} images as IM") from e
+
+ frames = im.encoderinfo.get("frames", 1)
+
+ fp.write(f"Image type: {image_type} image\r\n".encode("ascii"))
+ if filename:
+ # Each line must be 100 characters or less,
+ # or: SyntaxError("not an IM file")
+ # 8 characters are used for "Name: " and "\r\n"
+ # Keep just the filename, ditch the potentially overlong path
+ name, ext = os.path.splitext(os.path.basename(filename))
+ name = "".join([name[: 92 - len(ext)], ext])
+
+ fp.write(f"Name: {name}\r\n".encode("ascii"))
+ fp.write(("Image size (x*y): %d*%d\r\n" % im.size).encode("ascii"))
+ fp.write(f"File size (no of images): {frames}\r\n".encode("ascii"))
+ if im.mode in ["P", "PA"]:
+ fp.write(b"Lut: 1\r\n")
+ fp.write(b"\000" * (511 - fp.tell()) + b"\032")
+ if im.mode in ["P", "PA"]:
+ fp.write(im.im.getpalette("RGB", "RGB;L")) # 768 bytes
+ ImageFile._save(im, fp, [("raw", (0, 0) + im.size, 0, (rawmode, 0, -1))])
+
+
+#
+# --------------------------------------------------------------------
+# Registry
+
+
+Image.register_open(ImImageFile.format, ImImageFile)
+Image.register_save(ImImageFile.format, _save)
+
+Image.register_extension(ImImageFile.format, ".im")
diff --git a/venv/Lib/site-packages/PIL/Image.py b/venv/Lib/site-packages/PIL/Image.py
new file mode 100644
index 0000000..813ac52
--- /dev/null
+++ b/venv/Lib/site-packages/PIL/Image.py
@@ -0,0 +1,3697 @@
+#
+# The Python Imaging Library.
+# $Id$
+#
+# the Image class wrapper
+#
+# partial release history:
+# 1995-09-09 fl Created
+# 1996-03-11 fl PIL release 0.0 (proof of concept)
+# 1996-04-30 fl PIL release 0.1b1
+# 1999-07-28 fl PIL release 1.0 final
+# 2000-06-07 fl PIL release 1.1
+# 2000-10-20 fl PIL release 1.1.1
+# 2001-05-07 fl PIL release 1.1.2
+# 2002-03-15 fl PIL release 1.1.3
+# 2003-05-10 fl PIL release 1.1.4
+# 2005-03-28 fl PIL release 1.1.5
+# 2006-12-02 fl PIL release 1.1.6
+# 2009-11-15 fl PIL release 1.1.7
+#
+# Copyright (c) 1997-2009 by Secret Labs AB. All rights reserved.
+# Copyright (c) 1995-2009 by Fredrik Lundh.
+#
+# See the README file for information on usage and redistribution.
+#
+
+import atexit
+import builtins
+import io
+import logging
+import math
+import numbers
+import os
+import re
+import struct
+import sys
+import tempfile
+import warnings
+from collections.abc import Callable, MutableMapping
+from enum import IntEnum
+from pathlib import Path
+
+try:
+ import defusedxml.ElementTree as ElementTree
+except ImportError:
+ ElementTree = None
+
+# VERSION was removed in Pillow 6.0.0.
+# PILLOW_VERSION was removed in Pillow 9.0.0.
+# Use __version__ instead.
+from . import ImageMode, TiffTags, UnidentifiedImageError, __version__, _plugins
+from ._binary import i32le, o32be, o32le
+from ._util import deferred_error, isPath
+
+
+def __getattr__(name):
+ deprecated = "deprecated and will be removed in Pillow 10 (2023-07-01). "
+ categories = {"NORMAL": 0, "SEQUENCE": 1, "CONTAINER": 2}
+ if name in categories:
+ warnings.warn(
+ "Image categories are " + deprecated + "Use is_animated instead.",
+ DeprecationWarning,
+ stacklevel=2,
+ )
+ return categories[name]
+ elif name in ("NEAREST", "NONE"):
+ warnings.warn(
+ name
+ + " is "
+ + deprecated
+ + "Use Resampling.NEAREST or Dither.NONE instead.",
+ DeprecationWarning,
+ stacklevel=2,
+ )
+ return 0
+ old_resampling = {
+ "LINEAR": "BILINEAR",
+ "CUBIC": "BICUBIC",
+ "ANTIALIAS": "LANCZOS",
+ }
+ if name in old_resampling:
+ warnings.warn(
+ name
+ + " is "
+ + deprecated
+ + "Use Resampling."
+ + old_resampling[name]
+ + " instead.",
+ DeprecationWarning,
+ stacklevel=2,
+ )
+ return Resampling[old_resampling[name]]
+ for enum in (Transpose, Transform, Resampling, Dither, Palette, Quantize):
+ if name in enum.__members__:
+ warnings.warn(
+ name
+ + " is "
+ + deprecated
+ + "Use "
+ + enum.__name__
+ + "."
+ + name
+ + " instead.",
+ DeprecationWarning,
+ stacklevel=2,
+ )
+ return enum[name]
+ raise AttributeError(f"module '{__name__}' has no attribute '{name}'")
+
+
+logger = logging.getLogger(__name__)
+
+
+class DecompressionBombWarning(RuntimeWarning):
+ pass
+
+
+class DecompressionBombError(Exception):
+ pass
+
+
+# Limit to around a quarter gigabyte for a 24-bit (3 bpp) image
+MAX_IMAGE_PIXELS = int(1024 * 1024 * 1024 // 4 // 3)
+
+
+try:
+ # If the _imaging C module is not present, Pillow will not load.
+ # Note that other modules should not refer to _imaging directly;
+ # import Image and use the Image.core variable instead.
+ # Also note that Image.core is not a publicly documented interface,
+ # and should be considered private and subject to change.
+ from . import _imaging as core
+
+ if __version__ != getattr(core, "PILLOW_VERSION", None):
+ raise ImportError(
+ "The _imaging extension was built for another version of Pillow or PIL:\n"
+ f"Core version: {getattr(core, 'PILLOW_VERSION', None)}\n"
+ f"Pillow version: {__version__}"
+ )
+
+except ImportError as v:
+ core = deferred_error(ImportError("The _imaging C module is not installed."))
+ # Explanations for ways that we know we might have an import error
+ if str(v).startswith("Module use of python"):
+ # The _imaging C module is present, but not compiled for
+ # the right version (windows only). Print a warning, if
+ # possible.
+ warnings.warn(
+ "The _imaging extension was built for another version of Python.",
+ RuntimeWarning,
+ )
+ elif str(v).startswith("The _imaging extension"):
+ warnings.warn(str(v), RuntimeWarning)
+ # Fail here anyway. Don't let people run with a mostly broken Pillow.
+ # see docs/porting.rst
+ raise
+
+
+# works everywhere, win for pypy, not cpython
+USE_CFFI_ACCESS = hasattr(sys, "pypy_version_info")
+try:
+ import cffi
+except ImportError:
+ cffi = None
+
+
+def isImageType(t):
+ """
+ Checks if an object is an image object.
+
+ .. warning::
+
+ This function is for internal use only.
+
+ :param t: object to check if it's an image
+ :returns: True if the object is an image
+ """
+ return hasattr(t, "im")
+
+
+#
+# Constants
+
+# transpose
+class Transpose(IntEnum):
+ FLIP_LEFT_RIGHT = 0
+ FLIP_TOP_BOTTOM = 1
+ ROTATE_90 = 2
+ ROTATE_180 = 3
+ ROTATE_270 = 4
+ TRANSPOSE = 5
+ TRANSVERSE = 6
+
+
+# transforms (also defined in Imaging.h)
+class Transform(IntEnum):
+ AFFINE = 0
+ EXTENT = 1
+ PERSPECTIVE = 2
+ QUAD = 3
+ MESH = 4
+
+
+# resampling filters (also defined in Imaging.h)
+class Resampling(IntEnum):
+ NEAREST = 0
+ BOX = 4
+ BILINEAR = 2
+ HAMMING = 5
+ BICUBIC = 3
+ LANCZOS = 1
+
+
+_filters_support = {
+ Resampling.BOX: 0.5,
+ Resampling.BILINEAR: 1.0,
+ Resampling.HAMMING: 1.0,
+ Resampling.BICUBIC: 2.0,
+ Resampling.LANCZOS: 3.0,
+}
+
+
+# dithers
+class Dither(IntEnum):
+ NONE = 0
+ ORDERED = 1 # Not yet implemented
+ RASTERIZE = 2 # Not yet implemented
+ FLOYDSTEINBERG = 3 # default
+
+
+# palettes/quantizers
+class Palette(IntEnum):
+ WEB = 0
+ ADAPTIVE = 1
+
+
+class Quantize(IntEnum):
+ MEDIANCUT = 0
+ MAXCOVERAGE = 1
+ FASTOCTREE = 2
+ LIBIMAGEQUANT = 3
+
+
+if hasattr(core, "DEFAULT_STRATEGY"):
+ DEFAULT_STRATEGY = core.DEFAULT_STRATEGY
+ FILTERED = core.FILTERED
+ HUFFMAN_ONLY = core.HUFFMAN_ONLY
+ RLE = core.RLE
+ FIXED = core.FIXED
+
+
+# --------------------------------------------------------------------
+# Registries
+
+ID = []
+OPEN = {}
+MIME = {}
+SAVE = {}
+SAVE_ALL = {}
+EXTENSION = {}
+DECODERS = {}
+ENCODERS = {}
+
+# --------------------------------------------------------------------
+# Modes
+
+_ENDIAN = "<" if sys.byteorder == "little" else ">"
+
+
+def _conv_type_shape(im):
+ m = ImageMode.getmode(im.mode)
+ shape = (im.height, im.width)
+ extra = len(m.bands)
+ if extra != 1:
+ shape += (extra,)
+ return shape, m.typestr
+
+
+MODES = ["1", "CMYK", "F", "HSV", "I", "L", "LAB", "P", "RGB", "RGBA", "RGBX", "YCbCr"]
+
+# raw modes that may be memory mapped. NOTE: if you change this, you
+# may have to modify the stride calculation in map.c too!
+_MAPMODES = ("L", "P", "RGBX", "RGBA", "CMYK", "I;16", "I;16L", "I;16B")
+
+
+def getmodebase(mode):
+ """
+ Gets the "base" mode for given mode. This function returns "L" for
+ images that contain grayscale data, and "RGB" for images that
+ contain color data.
+
+ :param mode: Input mode.
+ :returns: "L" or "RGB".
+ :exception KeyError: If the input mode was not a standard mode.
+ """
+ return ImageMode.getmode(mode).basemode
+
+
+def getmodetype(mode):
+ """
+ Gets the storage type mode. Given a mode, this function returns a
+ single-layer mode suitable for storing individual bands.
+
+ :param mode: Input mode.
+ :returns: "L", "I", or "F".
+ :exception KeyError: If the input mode was not a standard mode.
+ """
+ return ImageMode.getmode(mode).basetype
+
+
+def getmodebandnames(mode):
+ """
+ Gets a list of individual band names. Given a mode, this function returns
+ a tuple containing the names of individual bands (use
+ :py:method:`~PIL.Image.getmodetype` to get the mode used to store each
+ individual band.
+
+ :param mode: Input mode.
+ :returns: A tuple containing band names. The length of the tuple
+ gives the number of bands in an image of the given mode.
+ :exception KeyError: If the input mode was not a standard mode.
+ """
+ return ImageMode.getmode(mode).bands
+
+
+def getmodebands(mode):
+ """
+ Gets the number of individual bands for this mode.
+
+ :param mode: Input mode.
+ :returns: The number of bands in this mode.
+ :exception KeyError: If the input mode was not a standard mode.
+ """
+ return len(ImageMode.getmode(mode).bands)
+
+
+# --------------------------------------------------------------------
+# Helpers
+
+_initialized = 0
+
+
+def preinit():
+ """Explicitly load standard file format drivers."""
+
+ global _initialized
+ if _initialized >= 1:
+ return
+
+ try:
+ from . import BmpImagePlugin
+
+ assert BmpImagePlugin
+ except ImportError:
+ pass
+ try:
+ from . import GifImagePlugin
+
+ assert GifImagePlugin
+ except ImportError:
+ pass
+ try:
+ from . import JpegImagePlugin
+
+ assert JpegImagePlugin
+ except ImportError:
+ pass
+ try:
+ from . import PpmImagePlugin
+
+ assert PpmImagePlugin
+ except ImportError:
+ pass
+ try:
+ from . import PngImagePlugin
+
+ assert PngImagePlugin
+ except ImportError:
+ pass
+ # try:
+ # import TiffImagePlugin
+ # assert TiffImagePlugin
+ # except ImportError:
+ # pass
+
+ _initialized = 1
+
+
+def init():
+ """
+ Explicitly initializes the Python Imaging Library. This function
+ loads all available file format drivers.
+ """
+
+ global _initialized
+ if _initialized >= 2:
+ return 0
+
+ for plugin in _plugins:
+ try:
+ logger.debug("Importing %s", plugin)
+ __import__(f"PIL.{plugin}", globals(), locals(), [])
+ except ImportError as e:
+ logger.debug("Image: failed to import %s: %s", plugin, e)
+
+ if OPEN or SAVE:
+ _initialized = 2
+ return 1
+
+
+# --------------------------------------------------------------------
+# Codec factories (used by tobytes/frombytes and ImageFile.load)
+
+
+def _getdecoder(mode, decoder_name, args, extra=()):
+
+ # tweak arguments
+ if args is None:
+ args = ()
+ elif not isinstance(args, tuple):
+ args = (args,)
+
+ try:
+ decoder = DECODERS[decoder_name]
+ except KeyError:
+ pass
+ else:
+ return decoder(mode, *args + extra)
+
+ try:
+ # get decoder
+ decoder = getattr(core, decoder_name + "_decoder")
+ except AttributeError as e:
+ raise OSError(f"decoder {decoder_name} not available") from e
+ return decoder(mode, *args + extra)
+
+
+def _getencoder(mode, encoder_name, args, extra=()):
+
+ # tweak arguments
+ if args is None:
+ args = ()
+ elif not isinstance(args, tuple):
+ args = (args,)
+
+ try:
+ encoder = ENCODERS[encoder_name]
+ except KeyError:
+ pass
+ else:
+ return encoder(mode, *args + extra)
+
+ try:
+ # get encoder
+ encoder = getattr(core, encoder_name + "_encoder")
+ except AttributeError as e:
+ raise OSError(f"encoder {encoder_name} not available") from e
+ return encoder(mode, *args + extra)
+
+
+# --------------------------------------------------------------------
+# Simple expression analyzer
+
+
+def coerce_e(value):
+ return value if isinstance(value, _E) else _E(value)
+
+
+class _E:
+ def __init__(self, data):
+ self.data = data
+
+ def __add__(self, other):
+ return _E((self.data, "__add__", coerce_e(other).data))
+
+ def __mul__(self, other):
+ return _E((self.data, "__mul__", coerce_e(other).data))
+
+
+def _getscaleoffset(expr):
+ stub = ["stub"]
+ data = expr(_E(stub)).data
+ try:
+ (a, b, c) = data # simplified syntax
+ if a is stub and b == "__mul__" and isinstance(c, numbers.Number):
+ return c, 0.0
+ if a is stub and b == "__add__" and isinstance(c, numbers.Number):
+ return 1.0, c
+ except TypeError:
+ pass
+ try:
+ ((a, b, c), d, e) = data # full syntax
+ if (
+ a is stub
+ and b == "__mul__"
+ and isinstance(c, numbers.Number)
+ and d == "__add__"
+ and isinstance(e, numbers.Number)
+ ):
+ return c, e
+ except TypeError:
+ pass
+ raise ValueError("illegal expression")
+
+
+# --------------------------------------------------------------------
+# Implementation wrapper
+
+
+class Image:
+ """
+ This class represents an image object. To create
+ :py:class:`~PIL.Image.Image` objects, use the appropriate factory
+ functions. There's hardly ever any reason to call the Image constructor
+ directly.
+
+ * :py:func:`~PIL.Image.open`
+ * :py:func:`~PIL.Image.new`
+ * :py:func:`~PIL.Image.frombytes`
+ """
+
+ format = None
+ format_description = None
+ _close_exclusive_fp_after_loading = True
+
+ def __init__(self):
+ # FIXME: take "new" parameters / other image?
+ # FIXME: turn mode and size into delegating properties?
+ self.im = None
+ self.mode = ""
+ self._size = (0, 0)
+ self.palette = None
+ self.info = {}
+ self._category = 0
+ self.readonly = 0
+ self.pyaccess = None
+ self._exif = None
+
+ def __getattr__(self, name):
+ if name == "category":
+ warnings.warn(
+ "Image categories are deprecated and will be removed in Pillow 10 "
+ "(2023-07-01). Use is_animated instead.",
+ DeprecationWarning,
+ stacklevel=2,
+ )
+ return self._category
+ raise AttributeError(name)
+
+ @property
+ def width(self):
+ return self.size[0]
+
+ @property
+ def height(self):
+ return self.size[1]
+
+ @property
+ def size(self):
+ return self._size
+
+ def _new(self, im):
+ new = Image()
+ new.im = im
+ new.mode = im.mode
+ new._size = im.size
+ if im.mode in ("P", "PA"):
+ if self.palette:
+ new.palette = self.palette.copy()
+ else:
+ from . import ImagePalette
+
+ new.palette = ImagePalette.ImagePalette()
+ new.info = self.info.copy()
+ return new
+
+ # Context manager support
+ def __enter__(self):
+ return self
+
+ def __exit__(self, *args):
+ if hasattr(self, "fp") and getattr(self, "_exclusive_fp", False):
+ if hasattr(self, "_close__fp"):
+ self._close__fp()
+ if self.fp:
+ self.fp.close()
+ self.fp = None
+
+ def close(self):
+ """
+ Closes the file pointer, if possible.
+
+ This operation will destroy the image core and release its memory.
+ The image data will be unusable afterward.
+
+ This function is required to close images that have multiple frames or
+ have not had their file read and closed by the
+ :py:meth:`~PIL.Image.Image.load` method. See :ref:`file-handling` for
+ more information.
+ """
+ try:
+ if hasattr(self, "_close__fp"):
+ self._close__fp()
+ if self.fp:
+ self.fp.close()
+ self.fp = None
+ except Exception as msg:
+ logger.debug("Error closing: %s", msg)
+
+ if getattr(self, "map", None):
+ self.map = None
+
+ # Instead of simply setting to None, we're setting up a
+ # deferred error that will better explain that the core image
+ # object is gone.
+ self.im = deferred_error(ValueError("Operation on closed image"))
+
+ def _copy(self):
+ self.load()
+ self.im = self.im.copy()
+ self.pyaccess = None
+ self.readonly = 0
+
+ def _ensure_mutable(self):
+ if self.readonly:
+ self._copy()
+ else:
+ self.load()
+
+ def _dump(self, file=None, format=None, **options):
+ suffix = ""
+ if format:
+ suffix = "." + format
+
+ if not file:
+ f, filename = tempfile.mkstemp(suffix)
+ os.close(f)
+ else:
+ filename = file
+ if not filename.endswith(suffix):
+ filename = filename + suffix
+
+ self.load()
+
+ if not format or format == "PPM":
+ self.im.save_ppm(filename)
+ else:
+ self.save(filename, format, **options)
+
+ return filename
+
+ def __eq__(self, other):
+ return (
+ self.__class__ is other.__class__
+ and self.mode == other.mode
+ and self.size == other.size
+ and self.info == other.info
+ and self._category == other._category
+ and self.getpalette() == other.getpalette()
+ and self.tobytes() == other.tobytes()
+ )
+
+ def __repr__(self):
+ return "<%s.%s image mode=%s size=%dx%d at 0x%X>" % (
+ self.__class__.__module__,
+ self.__class__.__name__,
+ self.mode,
+ self.size[0],
+ self.size[1],
+ id(self),
+ )
+
+ def _repr_pretty_(self, p, cycle):
+ """IPython plain text display support"""
+
+ # Same as __repr__ but without unpredicatable id(self),
+ # to keep Jupyter notebook `text/plain` output stable.
+ p.text(
+ "<%s.%s image mode=%s size=%dx%d>"
+ % (
+ self.__class__.__module__,
+ self.__class__.__name__,
+ self.mode,
+ self.size[0],
+ self.size[1],
+ )
+ )
+
+ def _repr_png_(self):
+ """iPython display hook support
+
+ :returns: png version of the image as bytes
+ """
+ b = io.BytesIO()
+ try:
+ self.save(b, "PNG")
+ except Exception as e:
+ raise ValueError("Could not save to PNG for display") from e
+ return b.getvalue()
+
+ class _ArrayData:
+ def __init__(self, new):
+ self.__array_interface__ = new
+
+ def __array__(self, dtype=None):
+ # numpy array interface support
+ import numpy as np
+
+ new = {}
+ shape, typestr = _conv_type_shape(self)
+ new["shape"] = shape
+ new["typestr"] = typestr
+ new["version"] = 3
+ if self.mode == "1":
+ # Binary images need to be extended from bits to bytes
+ # See: https://github.com/python-pillow/Pillow/issues/350
+ new["data"] = self.tobytes("raw", "L")
+ else:
+ new["data"] = self.tobytes()
+
+ return np.array(self._ArrayData(new), dtype)
+
+ def __getstate__(self):
+ return [self.info, self.mode, self.size, self.getpalette(), self.tobytes()]
+
+ def __setstate__(self, state):
+ Image.__init__(self)
+ self.tile = []
+ info, mode, size, palette, data = state
+ self.info = info
+ self.mode = mode
+ self._size = size
+ self.im = core.new(mode, size)
+ if mode in ("L", "LA", "P", "PA") and palette:
+ self.putpalette(palette)
+ self.frombytes(data)
+
+ def tobytes(self, encoder_name="raw", *args):
+ """
+ Return image as a bytes object.
+
+ .. warning::
+
+ This method returns the raw image data from the internal
+ storage. For compressed image data (e.g. PNG, JPEG) use
+ :meth:`~.save`, with a BytesIO parameter for in-memory
+ data.
+
+ :param encoder_name: What encoder to use. The default is to
+ use the standard "raw" encoder.
+ :param args: Extra arguments to the encoder.
+ :returns: A :py:class:`bytes` object.
+ """
+
+ # may pass tuple instead of argument list
+ if len(args) == 1 and isinstance(args[0], tuple):
+ args = args[0]
+
+ if encoder_name == "raw" and args == ():
+ args = self.mode
+
+ self.load()
+
+ if self.width == 0 or self.height == 0:
+ return b""
+
+ # unpack data
+ e = _getencoder(self.mode, encoder_name, args)
+ e.setimage(self.im)
+
+ bufsize = max(65536, self.size[0] * 4) # see RawEncode.c
+
+ data = []
+ while True:
+ l, s, d = e.encode(bufsize)
+ data.append(d)
+ if s:
+ break
+ if s < 0:
+ raise RuntimeError(f"encoder error {s} in tobytes")
+
+ return b"".join(data)
+
+ def tobitmap(self, name="image"):
+ """
+ Returns the image converted to an X11 bitmap.
+
+ .. note:: This method only works for mode "1" images.
+
+ :param name: The name prefix to use for the bitmap variables.
+ :returns: A string containing an X11 bitmap.
+ :raises ValueError: If the mode is not "1"
+ """
+
+ self.load()
+ if self.mode != "1":
+ raise ValueError("not a bitmap")
+ data = self.tobytes("xbm")
+ return b"".join(
+ [
+ f"#define {name}_width {self.size[0]}\n".encode("ascii"),
+ f"#define {name}_height {self.size[1]}\n".encode("ascii"),
+ f"static char {name}_bits[] = {{\n".encode("ascii"),
+ data,
+ b"};",
+ ]
+ )
+
+ def frombytes(self, data, decoder_name="raw", *args):
+ """
+ Loads this image with pixel data from a bytes object.
+
+ This method is similar to the :py:func:`~PIL.Image.frombytes` function,
+ but loads data into this image instead of creating a new image object.
+ """
+
+ # may pass tuple instead of argument list
+ if len(args) == 1 and isinstance(args[0], tuple):
+ args = args[0]
+
+ # default format
+ if decoder_name == "raw" and args == ():
+ args = self.mode
+
+ # unpack data
+ d = _getdecoder(self.mode, decoder_name, args)
+ d.setimage(self.im)
+ s = d.decode(data)
+
+ if s[0] >= 0:
+ raise ValueError("not enough image data")
+ if s[1] != 0:
+ raise ValueError("cannot decode image data")
+
+ def load(self):
+ """
+ Allocates storage for the image and loads the pixel data. In
+ normal cases, you don't need to call this method, since the
+ Image class automatically loads an opened image when it is
+ accessed for the first time.
+
+ If the file associated with the image was opened by Pillow, then this
+ method will close it. The exception to this is if the image has
+ multiple frames, in which case the file will be left open for seek
+ operations. See :ref:`file-handling` for more information.
+
+ :returns: An image access object.
+ :rtype: :ref:`PixelAccess` or :py:class:`PIL.PyAccess`
+ """
+ if self.im is not None and self.palette and self.palette.dirty:
+ # realize palette
+ mode, arr = self.palette.getdata()
+ self.im.putpalette(mode, arr)
+ self.palette.dirty = 0
+ self.palette.rawmode = None
+ if "transparency" in self.info and mode in ("LA", "PA"):
+ if isinstance(self.info["transparency"], int):
+ self.im.putpalettealpha(self.info["transparency"], 0)
+ else:
+ self.im.putpalettealphas(self.info["transparency"])
+ self.palette.mode = "RGBA"
+ else:
+ palette_mode = "RGBA" if mode.startswith("RGBA") else "RGB"
+ self.palette.mode = palette_mode
+ self.palette.palette = self.im.getpalette(palette_mode, palette_mode)
+
+ if self.im is not None:
+ if cffi and USE_CFFI_ACCESS:
+ if self.pyaccess:
+ return self.pyaccess
+ from . import PyAccess
+
+ self.pyaccess = PyAccess.new(self, self.readonly)
+ if self.pyaccess:
+ return self.pyaccess
+ return self.im.pixel_access(self.readonly)
+
+ def verify(self):
+ """
+ Verifies the contents of a file. For data read from a file, this
+ method attempts to determine if the file is broken, without
+ actually decoding the image data. If this method finds any
+ problems, it raises suitable exceptions. If you need to load
+ the image after using this method, you must reopen the image
+ file.
+ """
+ pass
+
+ def convert(
+ self, mode=None, matrix=None, dither=None, palette=Palette.WEB, colors=256
+ ):
+ """
+ Returns a converted copy of this image. For the "P" mode, this
+ method translates pixels through the palette. If mode is
+ omitted, a mode is chosen so that all information in the image
+ and the palette can be represented without a palette.
+
+ The current version supports all possible conversions between
+ "L", "RGB" and "CMYK." The ``matrix`` argument only supports "L"
+ and "RGB".
+
+ When translating a color image to greyscale (mode "L"),
+ the library uses the ITU-R 601-2 luma transform::
+
+ L = R * 299/1000 + G * 587/1000 + B * 114/1000
+
+ The default method of converting a greyscale ("L") or "RGB"
+ image into a bilevel (mode "1") image uses Floyd-Steinberg
+ dither to approximate the original image luminosity levels. If
+ dither is ``None``, all values larger than 127 are set to 255 (white),
+ all other values to 0 (black). To use other thresholds, use the
+ :py:meth:`~PIL.Image.Image.point` method.
+
+ When converting from "RGBA" to "P" without a ``matrix`` argument,
+ this passes the operation to :py:meth:`~PIL.Image.Image.quantize`,
+ and ``dither`` and ``palette`` are ignored.
+
+ :param mode: The requested mode. See: :ref:`concept-modes`.
+ :param matrix: An optional conversion matrix. If given, this
+ should be 4- or 12-tuple containing floating point values.
+ :param dither: Dithering method, used when converting from
+ mode "RGB" to "P" or from "RGB" or "L" to "1".
+ Available methods are :data:`Dither.NONE` or :data:`Dither.FLOYDSTEINBERG`
+ (default). Note that this is not used when ``matrix`` is supplied.
+ :param palette: Palette to use when converting from mode "RGB"
+ to "P". Available palettes are :data:`Palette.WEB` or
+ :data:`Palette.ADAPTIVE`.
+ :param colors: Number of colors to use for the :data:`Palette.ADAPTIVE`
+ palette. Defaults to 256.
+ :rtype: :py:class:`~PIL.Image.Image`
+ :returns: An :py:class:`~PIL.Image.Image` object.
+ """
+
+ self.load()
+
+ has_transparency = self.info.get("transparency") is not None
+ if not mode and self.mode == "P":
+ # determine default mode
+ if self.palette:
+ mode = self.palette.mode
+ else:
+ mode = "RGB"
+ if mode == "RGB" and has_transparency:
+ mode = "RGBA"
+ if not mode or (mode == self.mode and not matrix):
+ return self.copy()
+
+ if matrix:
+ # matrix conversion
+ if mode not in ("L", "RGB"):
+ raise ValueError("illegal conversion")
+ im = self.im.convert_matrix(mode, matrix)
+ new = self._new(im)
+ if has_transparency and self.im.bands == 3:
+ transparency = new.info["transparency"]
+
+ def convert_transparency(m, v):
+ v = m[0] * v[0] + m[1] * v[1] + m[2] * v[2] + m[3] * 0.5
+ return max(0, min(255, int(v)))
+
+ if mode == "L":
+ transparency = convert_transparency(matrix, transparency)
+ elif len(mode) == 3:
+ transparency = tuple(
+ convert_transparency(matrix[i * 4 : i * 4 + 4], transparency)
+ for i in range(0, len(transparency))
+ )
+ new.info["transparency"] = transparency
+ return new
+
+ if mode == "P" and self.mode == "RGBA":
+ return self.quantize(colors)
+
+ trns = None
+ delete_trns = False
+ # transparency handling
+ if has_transparency:
+ if (self.mode in ("1", "L", "I") and mode in ("LA", "RGBA")) or (
+ self.mode == "RGB" and mode == "RGBA"
+ ):
+ # Use transparent conversion to promote from transparent
+ # color to an alpha channel.
+ new_im = self._new(
+ self.im.convert_transparent(mode, self.info["transparency"])
+ )
+ del new_im.info["transparency"]
+ return new_im
+ elif self.mode in ("L", "RGB", "P") and mode in ("L", "RGB", "P"):
+ t = self.info["transparency"]
+ if isinstance(t, bytes):
+ # Dragons. This can't be represented by a single color
+ warnings.warn(
+ "Palette images with Transparency expressed in bytes should be "
+ "converted to RGBA images"
+ )
+ delete_trns = True
+ else:
+ # get the new transparency color.
+ # use existing conversions
+ trns_im = Image()._new(core.new(self.mode, (1, 1)))
+ if self.mode == "P":
+ trns_im.putpalette(self.palette)
+ if isinstance(t, tuple):
+ err = "Couldn't allocate a palette color for transparency"
+ try:
+ t = trns_im.palette.getcolor(t, self)
+ except ValueError as e:
+ if str(e) == "cannot allocate more than 256 colors":
+ # If all 256 colors are in use,
+ # then there is no need for transparency
+ t = None
+ else:
+ raise ValueError(err) from e
+ if t is None:
+ trns = None
+ else:
+ trns_im.putpixel((0, 0), t)
+
+ if mode in ("L", "RGB"):
+ trns_im = trns_im.convert(mode)
+ else:
+ # can't just retrieve the palette number, got to do it
+ # after quantization.
+ trns_im = trns_im.convert("RGB")
+ trns = trns_im.getpixel((0, 0))
+
+ elif self.mode == "P" and mode in ("LA", "PA", "RGBA"):
+ t = self.info["transparency"]
+ delete_trns = True
+
+ if isinstance(t, bytes):
+ self.im.putpalettealphas(t)
+ elif isinstance(t, int):
+ self.im.putpalettealpha(t, 0)
+ else:
+ raise ValueError("Transparency for P mode should be bytes or int")
+
+ if mode == "P" and palette == Palette.ADAPTIVE:
+ im = self.im.quantize(colors)
+ new = self._new(im)
+ from . import ImagePalette
+
+ new.palette = ImagePalette.ImagePalette("RGB", new.im.getpalette("RGB"))
+ if delete_trns:
+ # This could possibly happen if we requantize to fewer colors.
+ # The transparency would be totally off in that case.
+ del new.info["transparency"]
+ if trns is not None:
+ try:
+ new.info["transparency"] = new.palette.getcolor(trns, new)
+ except Exception:
+ # if we can't make a transparent color, don't leave the old
+ # transparency hanging around to mess us up.
+ del new.info["transparency"]
+ warnings.warn("Couldn't allocate palette entry for transparency")
+ return new
+
+ # colorspace conversion
+ if dither is None:
+ dither = Dither.FLOYDSTEINBERG
+
+ try:
+ im = self.im.convert(mode, dither)
+ except ValueError:
+ try:
+ # normalize source image and try again
+ im = self.im.convert(getmodebase(self.mode))
+ im = im.convert(mode, dither)
+ except KeyError as e:
+ raise ValueError("illegal conversion") from e
+
+ new_im = self._new(im)
+ if mode == "P" and palette != Palette.ADAPTIVE:
+ from . import ImagePalette
+
+ new_im.palette = ImagePalette.ImagePalette("RGB", list(range(256)) * 3)
+ if delete_trns:
+ # crash fail if we leave a bytes transparency in an rgb/l mode.
+ del new_im.info["transparency"]
+ if trns is not None:
+ if new_im.mode == "P":
+ try:
+ new_im.info["transparency"] = new_im.palette.getcolor(trns, new_im)
+ except ValueError as e:
+ del new_im.info["transparency"]
+ if str(e) != "cannot allocate more than 256 colors":
+ # If all 256 colors are in use,
+ # then there is no need for transparency
+ warnings.warn(
+ "Couldn't allocate palette entry for transparency"
+ )
+ else:
+ new_im.info["transparency"] = trns
+ return new_im
+
+ def quantize(
+ self,
+ colors=256,
+ method=None,
+ kmeans=0,
+ palette=None,
+ dither=Dither.FLOYDSTEINBERG,
+ ):
+ """
+ Convert the image to 'P' mode with the specified number
+ of colors.
+
+ :param colors: The desired number of colors, <= 256
+ :param method: :data:`Quantize.MEDIANCUT` (median cut),
+ :data:`Quantize.MAXCOVERAGE` (maximum coverage),
+ :data:`Quantize.FASTOCTREE` (fast octree),
+ :data:`Quantize.LIBIMAGEQUANT` (libimagequant; check support
+ using :py:func:`PIL.features.check_feature` with
+ ``feature="libimagequant"``).
+
+ By default, :data:`Quantize.MEDIANCUT` will be used.
+
+ The exception to this is RGBA images. :data:`Quantize.MEDIANCUT`
+ and :data:`Quantize.MAXCOVERAGE` do not support RGBA images, so
+ :data:`Quantize.FASTOCTREE` is used by default instead.
+ :param kmeans: Integer
+ :param palette: Quantize to the palette of given
+ :py:class:`PIL.Image.Image`.
+ :param dither: Dithering method, used when converting from
+ mode "RGB" to "P" or from "RGB" or "L" to "1".
+ Available methods are :data:`Dither.NONE` or :data:`Dither.FLOYDSTEINBERG`
+ (default).
+ :returns: A new image
+
+ """
+
+ self.load()
+
+ if method is None:
+ # defaults:
+ method = Quantize.MEDIANCUT
+ if self.mode == "RGBA":
+ method = Quantize.FASTOCTREE
+
+ if self.mode == "RGBA" and method not in (
+ Quantize.FASTOCTREE,
+ Quantize.LIBIMAGEQUANT,
+ ):
+ # Caller specified an invalid mode.
+ raise ValueError(
+ "Fast Octree (method == 2) and libimagequant (method == 3) "
+ "are the only valid methods for quantizing RGBA images"
+ )
+
+ if palette:
+ # use palette from reference image
+ palette.load()
+ if palette.mode != "P":
+ raise ValueError("bad mode for palette image")
+ if self.mode != "RGB" and self.mode != "L":
+ raise ValueError(
+ "only RGB or L mode images can be quantized to a palette"
+ )
+ im = self.im.convert("P", dither, palette.im)
+ new_im = self._new(im)
+ new_im.palette = palette.palette.copy()
+ return new_im
+
+ im = self._new(self.im.quantize(colors, method, kmeans))
+
+ from . import ImagePalette
+
+ mode = im.im.getpalettemode()
+ palette = im.im.getpalette(mode, mode)[: colors * len(mode)]
+ im.palette = ImagePalette.ImagePalette(mode, palette)
+
+ return im
+
+ def copy(self):
+ """
+ Copies this image. Use this method if you wish to paste things
+ into an image, but still retain the original.
+
+ :rtype: :py:class:`~PIL.Image.Image`
+ :returns: An :py:class:`~PIL.Image.Image` object.
+ """
+ self.load()
+ return self._new(self.im.copy())
+
+ __copy__ = copy
+
+ def crop(self, box=None):
+ """
+ Returns a rectangular region from this image. The box is a
+ 4-tuple defining the left, upper, right, and lower pixel
+ coordinate. See :ref:`coordinate-system`.
+
+ Note: Prior to Pillow 3.4.0, this was a lazy operation.
+
+ :param box: The crop rectangle, as a (left, upper, right, lower)-tuple.
+ :rtype: :py:class:`~PIL.Image.Image`
+ :returns: An :py:class:`~PIL.Image.Image` object.
+ """
+
+ if box is None:
+ return self.copy()
+
+ if box[2] < box[0]:
+ raise ValueError("Coordinate 'right' is less than 'left'")
+ elif box[3] < box[1]:
+ raise ValueError("Coordinate 'lower' is less than 'upper'")
+
+ self.load()
+ return self._new(self._crop(self.im, box))
+
+ def _crop(self, im, box):
+ """
+ Returns a rectangular region from the core image object im.
+
+ This is equivalent to calling im.crop((x0, y0, x1, y1)), but
+ includes additional sanity checks.
+
+ :param im: a core image object
+ :param box: The crop rectangle, as a (left, upper, right, lower)-tuple.
+ :returns: A core image object.
+ """
+
+ x0, y0, x1, y1 = map(int, map(round, box))
+
+ absolute_values = (abs(x1 - x0), abs(y1 - y0))
+
+ _decompression_bomb_check(absolute_values)
+
+ return im.crop((x0, y0, x1, y1))
+
+ def draft(self, mode, size):
+ """
+ Configures the image file loader so it returns a version of the
+ image that as closely as possible matches the given mode and
+ size. For example, you can use this method to convert a color
+ JPEG to greyscale while loading it.
+
+ If any changes are made, returns a tuple with the chosen ``mode`` and
+ ``box`` with coordinates of the original image within the altered one.
+
+ Note that this method modifies the :py:class:`~PIL.Image.Image` object
+ in place. If the image has already been loaded, this method has no
+ effect.
+
+ Note: This method is not implemented for most images. It is
+ currently implemented only for JPEG and MPO images.
+
+ :param mode: The requested mode.
+ :param size: The requested size.
+ """
+ pass
+
+ def _expand(self, xmargin, ymargin=None):
+ if ymargin is None:
+ ymargin = xmargin
+ self.load()
+ return self._new(self.im.expand(xmargin, ymargin, 0))
+
+ def filter(self, filter):
+ """
+ Filters this image using the given filter. For a list of
+ available filters, see the :py:mod:`~PIL.ImageFilter` module.
+
+ :param filter: Filter kernel.
+ :returns: An :py:class:`~PIL.Image.Image` object."""
+
+ from . import ImageFilter
+
+ self.load()
+
+ if isinstance(filter, Callable):
+ filter = filter()
+ if not hasattr(filter, "filter"):
+ raise TypeError(
+ "filter argument should be ImageFilter.Filter instance or class"
+ )
+
+ multiband = isinstance(filter, ImageFilter.MultibandFilter)
+ if self.im.bands == 1 or multiband:
+ return self._new(filter.filter(self.im))
+
+ ims = []
+ for c in range(self.im.bands):
+ ims.append(self._new(filter.filter(self.im.getband(c))))
+ return merge(self.mode, ims)
+
+ def getbands(self):
+ """
+ Returns a tuple containing the name of each band in this image.
+ For example, ``getbands`` on an RGB image returns ("R", "G", "B").
+
+ :returns: A tuple containing band names.
+ :rtype: tuple
+ """
+ return ImageMode.getmode(self.mode).bands
+
+ def getbbox(self):
+ """
+ Calculates the bounding box of the non-zero regions in the
+ image.
+
+ :returns: The bounding box is returned as a 4-tuple defining the
+ left, upper, right, and lower pixel coordinate. See
+ :ref:`coordinate-system`. If the image is completely empty, this
+ method returns None.
+
+ """
+
+ self.load()
+ return self.im.getbbox()
+
+ def getcolors(self, maxcolors=256):
+ """
+ Returns a list of colors used in this image.
+
+ The colors will be in the image's mode. For example, an RGB image will
+ return a tuple of (red, green, blue) color values, and a P image will
+ return the index of the color in the palette.
+
+ :param maxcolors: Maximum number of colors. If this number is
+ exceeded, this method returns None. The default limit is
+ 256 colors.
+ :returns: An unsorted list of (count, pixel) values.
+ """
+
+ self.load()
+ if self.mode in ("1", "L", "P"):
+ h = self.im.histogram()
+ out = []
+ for i in range(256):
+ if h[i]:
+ out.append((h[i], i))
+ if len(out) > maxcolors:
+ return None
+ return out
+ return self.im.getcolors(maxcolors)
+
+ def getdata(self, band=None):
+ """
+ Returns the contents of this image as a sequence object
+ containing pixel values. The sequence object is flattened, so
+ that values for line one follow directly after the values of
+ line zero, and so on.
+
+ Note that the sequence object returned by this method is an
+ internal PIL data type, which only supports certain sequence
+ operations. To convert it to an ordinary sequence (e.g. for
+ printing), use ``list(im.getdata())``.
+
+ :param band: What band to return. The default is to return
+ all bands. To return a single band, pass in the index
+ value (e.g. 0 to get the "R" band from an "RGB" image).
+ :returns: A sequence-like object.
+ """
+
+ self.load()
+ if band is not None:
+ return self.im.getband(band)
+ return self.im # could be abused
+
+ def getextrema(self):
+ """
+ Gets the the minimum and maximum pixel values for each band in
+ the image.
+
+ :returns: For a single-band image, a 2-tuple containing the
+ minimum and maximum pixel value. For a multi-band image,
+ a tuple containing one 2-tuple for each band.
+ """
+
+ self.load()
+ if self.im.bands > 1:
+ extrema = []
+ for i in range(self.im.bands):
+ extrema.append(self.im.getband(i).getextrema())
+ return tuple(extrema)
+ return self.im.getextrema()
+
+ def _getxmp(self, xmp_tags):
+ def get_name(tag):
+ return tag.split("}")[1]
+
+ def get_value(element):
+ value = {get_name(k): v for k, v in element.attrib.items()}
+ children = list(element)
+ if children:
+ for child in children:
+ name = get_name(child.tag)
+ child_value = get_value(child)
+ if name in value:
+ if not isinstance(value[name], list):
+ value[name] = [value[name]]
+ value[name].append(child_value)
+ else:
+ value[name] = child_value
+ elif value:
+ if element.text:
+ value["text"] = element.text
+ else:
+ return element.text
+ return value
+
+ if ElementTree is None:
+ warnings.warn("XMP data cannot be read without defusedxml dependency")
+ return {}
+ else:
+ root = ElementTree.fromstring(xmp_tags)
+ return {get_name(root.tag): get_value(root)}
+
+ def getexif(self):
+ if self._exif is None:
+ self._exif = Exif()
+
+ exif_info = self.info.get("exif")
+ if exif_info is None:
+ if "Raw profile type exif" in self.info:
+ exif_info = bytes.fromhex(
+ "".join(self.info["Raw profile type exif"].split("\n")[3:])
+ )
+ elif hasattr(self, "tag_v2"):
+ self._exif.bigtiff = self.tag_v2._bigtiff
+ self._exif.endian = self.tag_v2._endian
+ self._exif.load_from_fp(self.fp, self.tag_v2._offset)
+ if exif_info is not None:
+ self._exif.load(exif_info)
+
+ # XMP tags
+ if 0x0112 not in self._exif:
+ xmp_tags = self.info.get("XML:com.adobe.xmp")
+ if xmp_tags:
+ match = re.search(r'tiff:Orientation="([0-9])"', xmp_tags)
+ if match:
+ self._exif[0x0112] = int(match[1])
+
+ return self._exif
+
+ def getim(self):
+ """
+ Returns a capsule that points to the internal image memory.
+
+ :returns: A capsule object.
+ """
+
+ self.load()
+ return self.im.ptr
+
+ def getpalette(self, rawmode="RGB"):
+ """
+ Returns the image palette as a list.
+
+ :param rawmode: The mode in which to return the palette. ``None`` will
+ return the palette in its current mode.
+
+ .. versionadded:: 9.1.0
+
+ :returns: A list of color values [r, g, b, ...], or None if the
+ image has no palette.
+ """
+
+ self.load()
+ try:
+ mode = self.im.getpalettemode()
+ except ValueError:
+ return None # no palette
+ if rawmode is None:
+ rawmode = mode
+ return list(self.im.getpalette(mode, rawmode))
+
+ def getpixel(self, xy):
+ """
+ Returns the pixel value at a given position.
+
+ :param xy: The coordinate, given as (x, y). See
+ :ref:`coordinate-system`.
+ :returns: The pixel value. If the image is a multi-layer image,
+ this method returns a tuple.
+ """
+
+ self.load()
+ if self.pyaccess:
+ return self.pyaccess.getpixel(xy)
+ return self.im.getpixel(xy)
+
+ def getprojection(self):
+ """
+ Get projection to x and y axes
+
+ :returns: Two sequences, indicating where there are non-zero
+ pixels along the X-axis and the Y-axis, respectively.
+ """
+
+ self.load()
+ x, y = self.im.getprojection()
+ return list(x), list(y)
+
+ def histogram(self, mask=None, extrema=None):
+ """
+ Returns a histogram for the image. The histogram is returned as a
+ list of pixel counts, one for each pixel value in the source
+ image. Counts are grouped into 256 bins for each band, even if
+ the image has more than 8 bits per band. If the image has more
+ than one band, the histograms for all bands are concatenated (for
+ example, the histogram for an "RGB" image contains 768 values).
+
+ A bilevel image (mode "1") is treated as a greyscale ("L") image
+ by this method.
+
+ If a mask is provided, the method returns a histogram for those
+ parts of the image where the mask image is non-zero. The mask
+ image must have the same size as the image, and be either a
+ bi-level image (mode "1") or a greyscale image ("L").
+
+ :param mask: An optional mask.
+ :param extrema: An optional tuple of manually-specified extrema.
+ :returns: A list containing pixel counts.
+ """
+ self.load()
+ if mask:
+ mask.load()
+ return self.im.histogram((0, 0), mask.im)
+ if self.mode in ("I", "F"):
+ if extrema is None:
+ extrema = self.getextrema()
+ return self.im.histogram(extrema)
+ return self.im.histogram()
+
+ def entropy(self, mask=None, extrema=None):
+ """
+ Calculates and returns the entropy for the image.
+
+ A bilevel image (mode "1") is treated as a greyscale ("L")
+ image by this method.
+
+ If a mask is provided, the method employs the histogram for
+ those parts of the image where the mask image is non-zero.
+ The mask image must have the same size as the image, and be
+ either a bi-level image (mode "1") or a greyscale image ("L").
+
+ :param mask: An optional mask.
+ :param extrema: An optional tuple of manually-specified extrema.
+ :returns: A float value representing the image entropy
+ """
+ self.load()
+ if mask:
+ mask.load()
+ return self.im.entropy((0, 0), mask.im)
+ if self.mode in ("I", "F"):
+ if extrema is None:
+ extrema = self.getextrema()
+ return self.im.entropy(extrema)
+ return self.im.entropy()
+
+ def paste(self, im, box=None, mask=None):
+ """
+ Pastes another image into this image. The box argument is either
+ a 2-tuple giving the upper left corner, a 4-tuple defining the
+ left, upper, right, and lower pixel coordinate, or None (same as
+ (0, 0)). See :ref:`coordinate-system`. If a 4-tuple is given, the size
+ of the pasted image must match the size of the region.
+
+ If the modes don't match, the pasted image is converted to the mode of
+ this image (see the :py:meth:`~PIL.Image.Image.convert` method for
+ details).
+
+ Instead of an image, the source can be a integer or tuple
+ containing pixel values. The method then fills the region
+ with the given color. When creating RGB images, you can
+ also use color strings as supported by the ImageColor module.
+
+ If a mask is given, this method updates only the regions
+ indicated by the mask. You can use either "1", "L", "LA", "RGBA"
+ or "RGBa" images (if present, the alpha band is used as mask).
+ Where the mask is 255, the given image is copied as is. Where
+ the mask is 0, the current value is preserved. Intermediate
+ values will mix the two images together, including their alpha
+ channels if they have them.
+
+ See :py:meth:`~PIL.Image.Image.alpha_composite` if you want to
+ combine images with respect to their alpha channels.
+
+ :param im: Source image or pixel value (integer or tuple).
+ :param box: An optional 4-tuple giving the region to paste into.
+ If a 2-tuple is used instead, it's treated as the upper left
+ corner. If omitted or None, the source is pasted into the
+ upper left corner.
+
+ If an image is given as the second argument and there is no
+ third, the box defaults to (0, 0), and the second argument
+ is interpreted as a mask image.
+ :param mask: An optional mask image.
+ """
+
+ if isImageType(box) and mask is None:
+ # abbreviated paste(im, mask) syntax
+ mask = box
+ box = None
+
+ if box is None:
+ box = (0, 0)
+
+ if len(box) == 2:
+ # upper left corner given; get size from image or mask
+ if isImageType(im):
+ size = im.size
+ elif isImageType(mask):
+ size = mask.size
+ else:
+ # FIXME: use self.size here?
+ raise ValueError("cannot determine region size; use 4-item box")
+ box += (box[0] + size[0], box[1] + size[1])
+
+ if isinstance(im, str):
+ from . import ImageColor
+
+ im = ImageColor.getcolor(im, self.mode)
+
+ elif isImageType(im):
+ im.load()
+ if self.mode != im.mode:
+ if self.mode != "RGB" or im.mode not in ("LA", "RGBA", "RGBa"):
+ # should use an adapter for this!
+ im = im.convert(self.mode)
+ im = im.im
+
+ self._ensure_mutable()
+
+ if mask:
+ mask.load()
+ self.im.paste(im, box, mask.im)
+ else:
+ self.im.paste(im, box)
+
+ def alpha_composite(self, im, dest=(0, 0), source=(0, 0)):
+ """'In-place' analog of Image.alpha_composite. Composites an image
+ onto this image.
+
+ :param im: image to composite over this one
+ :param dest: Optional 2 tuple (left, top) specifying the upper
+ left corner in this (destination) image.
+ :param source: Optional 2 (left, top) tuple for the upper left
+ corner in the overlay source image, or 4 tuple (left, top, right,
+ bottom) for the bounds of the source rectangle
+
+ Performance Note: Not currently implemented in-place in the core layer.
+ """
+
+ if not isinstance(source, (list, tuple)):
+ raise ValueError("Source must be a tuple")
+ if not isinstance(dest, (list, tuple)):
+ raise ValueError("Destination must be a tuple")
+ if not len(source) in (2, 4):
+ raise ValueError("Source must be a 2 or 4-tuple")
+ if not len(dest) == 2:
+ raise ValueError("Destination must be a 2-tuple")
+ if min(source) < 0:
+ raise ValueError("Source must be non-negative")
+
+ if len(source) == 2:
+ source = source + im.size
+
+ # over image, crop if it's not the whole thing.
+ if source == (0, 0) + im.size:
+ overlay = im
+ else:
+ overlay = im.crop(source)
+
+ # target for the paste
+ box = dest + (dest[0] + overlay.width, dest[1] + overlay.height)
+
+ # destination image. don't copy if we're using the whole image.
+ if box == (0, 0) + self.size:
+ background = self
+ else:
+ background = self.crop(box)
+
+ result = alpha_composite(background, overlay)
+ self.paste(result, box)
+
+ def point(self, lut, mode=None):
+ """
+ Maps this image through a lookup table or function.
+
+ :param lut: A lookup table, containing 256 (or 65536 if
+ self.mode=="I" and mode == "L") values per band in the
+ image. A function can be used instead, it should take a
+ single argument. The function is called once for each
+ possible pixel value, and the resulting table is applied to
+ all bands of the image.
+
+ It may also be an :py:class:`~PIL.Image.ImagePointHandler`
+ object::
+
+ class Example(Image.ImagePointHandler):
+ def point(self, data):
+ # Return result
+ :param mode: Output mode (default is same as input). In the
+ current version, this can only be used if the source image
+ has mode "L" or "P", and the output has mode "1" or the
+ source image mode is "I" and the output mode is "L".
+ :returns: An :py:class:`~PIL.Image.Image` object.
+ """
+
+ self.load()
+
+ if isinstance(lut, ImagePointHandler):
+ return lut.point(self)
+
+ if callable(lut):
+ # if it isn't a list, it should be a function
+ if self.mode in ("I", "I;16", "F"):
+ # check if the function can be used with point_transform
+ # UNDONE wiredfool -- I think this prevents us from ever doing
+ # a gamma function point transform on > 8bit images.
+ scale, offset = _getscaleoffset(lut)
+ return self._new(self.im.point_transform(scale, offset))
+ # for other modes, convert the function to a table
+ lut = [lut(i) for i in range(256)] * self.im.bands
+
+ if self.mode == "F":
+ # FIXME: _imaging returns a confusing error message for this case
+ raise ValueError("point operation not supported for this mode")
+
+ return self._new(self.im.point(lut, mode))
+
+ def putalpha(self, alpha):
+ """
+ Adds or replaces the alpha layer in this image. If the image
+ does not have an alpha layer, it's converted to "LA" or "RGBA".
+ The new layer must be either "L" or "1".
+
+ :param alpha: The new alpha layer. This can either be an "L" or "1"
+ image having the same size as this image, or an integer or
+ other color value.
+ """
+
+ self._ensure_mutable()
+
+ if self.mode not in ("LA", "PA", "RGBA"):
+ # attempt to promote self to a matching alpha mode
+ try:
+ mode = getmodebase(self.mode) + "A"
+ try:
+ self.im.setmode(mode)
+ except (AttributeError, ValueError) as e:
+ # do things the hard way
+ im = self.im.convert(mode)
+ if im.mode not in ("LA", "PA", "RGBA"):
+ raise ValueError from e # sanity check
+ self.im = im
+ self.pyaccess = None
+ self.mode = self.im.mode
+ except KeyError as e:
+ raise ValueError("illegal image mode") from e
+
+ if self.mode in ("LA", "PA"):
+ band = 1
+ else:
+ band = 3
+
+ if isImageType(alpha):
+ # alpha layer
+ if alpha.mode not in ("1", "L"):
+ raise ValueError("illegal image mode")
+ alpha.load()
+ if alpha.mode == "1":
+ alpha = alpha.convert("L")
+ else:
+ # constant alpha
+ try:
+ self.im.fillband(band, alpha)
+ except (AttributeError, ValueError):
+ # do things the hard way
+ alpha = new("L", self.size, alpha)
+ else:
+ return
+
+ self.im.putband(alpha.im, band)
+
+ def putdata(self, data, scale=1.0, offset=0.0):
+ """
+ Copies pixel data from a flattened sequence object into the image. The
+ values should start at the upper left corner (0, 0), continue to the
+ end of the line, followed directly by the first value of the second
+ line, and so on. Data will be read until either the image or the
+ sequence ends. The scale and offset values are used to adjust the
+ sequence values: **pixel = value*scale + offset**.
+
+ :param data: A flattened sequence object.
+ :param scale: An optional scale value. The default is 1.0.
+ :param offset: An optional offset value. The default is 0.0.
+ """
+
+ self._ensure_mutable()
+
+ self.im.putdata(data, scale, offset)
+
+ def putpalette(self, data, rawmode="RGB"):
+ """
+ Attaches a palette to this image. The image must be a "P", "PA", "L"
+ or "LA" image.
+
+ The palette sequence must contain at most 256 colors, made up of one
+ integer value for each channel in the raw mode.
+ For example, if the raw mode is "RGB", then it can contain at most 768
+ values, made up of red, green and blue values for the corresponding pixel
+ index in the 256 colors.
+ If the raw mode is "RGBA", then it can contain at most 1024 values,
+ containing red, green, blue and alpha values.
+
+ Alternatively, an 8-bit string may be used instead of an integer sequence.
+
+ :param data: A palette sequence (either a list or a string).
+ :param rawmode: The raw mode of the palette. Either "RGB", "RGBA", or a mode
+ that can be transformed to "RGB" or "RGBA" (e.g. "R", "BGR;15", "RGBA;L").
+ """
+ from . import ImagePalette
+
+ if self.mode not in ("L", "LA", "P", "PA"):
+ raise ValueError("illegal image mode")
+ if isinstance(data, ImagePalette.ImagePalette):
+ palette = ImagePalette.raw(data.rawmode, data.palette)
+ else:
+ if not isinstance(data, bytes):
+ data = bytes(data)
+ palette = ImagePalette.raw(rawmode, data)
+ self.mode = "PA" if "A" in self.mode else "P"
+ self.palette = palette
+ self.palette.mode = "RGB"
+ self.load() # install new palette
+
+ def putpixel(self, xy, value):
+ """
+ Modifies the pixel at the given position. The color is given as
+ a single numerical value for single-band images, and a tuple for
+ multi-band images. In addition to this, RGB and RGBA tuples are
+ accepted for P images.
+
+ Note that this method is relatively slow. For more extensive changes,
+ use :py:meth:`~PIL.Image.Image.paste` or the :py:mod:`~PIL.ImageDraw`
+ module instead.
+
+ See:
+
+ * :py:meth:`~PIL.Image.Image.paste`
+ * :py:meth:`~PIL.Image.Image.putdata`
+ * :py:mod:`~PIL.ImageDraw`
+
+ :param xy: The pixel coordinate, given as (x, y). See
+ :ref:`coordinate-system`.
+ :param value: The pixel value.
+ """
+
+ if self.readonly:
+ self._copy()
+ self.load()
+
+ if self.pyaccess:
+ return self.pyaccess.putpixel(xy, value)
+
+ if (
+ self.mode == "P"
+ and isinstance(value, (list, tuple))
+ and len(value) in [3, 4]
+ ):
+ # RGB or RGBA value for a P image
+ value = self.palette.getcolor(value, self)
+ return self.im.putpixel(xy, value)
+
+ def remap_palette(self, dest_map, source_palette=None):
+ """
+ Rewrites the image to reorder the palette.
+
+ :param dest_map: A list of indexes into the original palette.
+ e.g. ``[1,0]`` would swap a two item palette, and ``list(range(256))``
+ is the identity transform.
+ :param source_palette: Bytes or None.
+ :returns: An :py:class:`~PIL.Image.Image` object.
+
+ """
+ from . import ImagePalette
+
+ if self.mode not in ("L", "P"):
+ raise ValueError("illegal image mode")
+
+ if source_palette is None:
+ if self.mode == "P":
+ self.load()
+ source_palette = self.im.getpalette("RGB")[:768]
+ else: # L-mode
+ source_palette = bytearray(i // 3 for i in range(768))
+
+ palette_bytes = b""
+ new_positions = [0] * 256
+
+ # pick only the used colors from the palette
+ for i, oldPosition in enumerate(dest_map):
+ palette_bytes += source_palette[oldPosition * 3 : oldPosition * 3 + 3]
+ new_positions[oldPosition] = i
+
+ # replace the palette color id of all pixel with the new id
+
+ # Palette images are [0..255], mapped through a 1 or 3
+ # byte/color map. We need to remap the whole image
+ # from palette 1 to palette 2. New_positions is
+ # an array of indexes into palette 1. Palette 2 is
+ # palette 1 with any holes removed.
+
+ # We're going to leverage the convert mechanism to use the
+ # C code to remap the image from palette 1 to palette 2,
+ # by forcing the source image into 'L' mode and adding a
+ # mapping 'L' mode palette, then converting back to 'L'
+ # sans palette thus converting the image bytes, then
+ # assigning the optimized RGB palette.
+
+ # perf reference, 9500x4000 gif, w/~135 colors
+ # 14 sec prepatch, 1 sec postpatch with optimization forced.
+
+ mapping_palette = bytearray(new_positions)
+
+ m_im = self.copy()
+ m_im.mode = "P"
+
+ m_im.palette = ImagePalette.ImagePalette("RGB", palette=mapping_palette * 3)
+ # possibly set palette dirty, then
+ # m_im.putpalette(mapping_palette, 'L') # converts to 'P'
+ # or just force it.
+ # UNDONE -- this is part of the general issue with palettes
+ m_im.im.putpalette("RGB;L", m_im.palette.tobytes())
+
+ m_im = m_im.convert("L")
+
+ # Internally, we require 768 bytes for a palette.
+ new_palette_bytes = palette_bytes + (768 - len(palette_bytes)) * b"\x00"
+ m_im.putpalette(new_palette_bytes)
+ m_im.palette = ImagePalette.ImagePalette("RGB", palette=palette_bytes)
+
+ return m_im
+
+ def _get_safe_box(self, size, resample, box):
+ """Expands the box so it includes adjacent pixels
+ that may be used by resampling with the given resampling filter.
+ """
+ filter_support = _filters_support[resample] - 0.5
+ scale_x = (box[2] - box[0]) / size[0]
+ scale_y = (box[3] - box[1]) / size[1]
+ support_x = filter_support * scale_x
+ support_y = filter_support * scale_y
+
+ return (
+ max(0, int(box[0] - support_x)),
+ max(0, int(box[1] - support_y)),
+ min(self.size[0], math.ceil(box[2] + support_x)),
+ min(self.size[1], math.ceil(box[3] + support_y)),
+ )
+
+ def resize(self, size, resample=None, box=None, reducing_gap=None):
+ """
+ Returns a resized copy of this image.
+
+ :param size: The requested size in pixels, as a 2-tuple:
+ (width, height).
+ :param resample: An optional resampling filter. This can be
+ one of :py:data:`PIL.Image.Resampling.NEAREST`,
+ :py:data:`PIL.Image.Resampling.BOX`,
+ :py:data:`PIL.Image.Resampling.BILINEAR`,
+ :py:data:`PIL.Image.Resampling.HAMMING`,
+ :py:data:`PIL.Image.Resampling.BICUBIC` or
+ :py:data:`PIL.Image.Resampling.LANCZOS`.
+ If the image has mode "1" or "P", it is always set to
+ :py:data:`PIL.Image.Resampling.NEAREST`.
+ If the image mode specifies a number of bits, such as "I;16", then the
+ default filter is :py:data:`PIL.Image.Resampling.NEAREST`.
+ Otherwise, the default filter is
+ :py:data:`PIL.Image.Resampling.BICUBIC`. See: :ref:`concept-filters`.
+ :param box: An optional 4-tuple of floats providing
+ the source image region to be scaled.
+ The values must be within (0, 0, width, height) rectangle.
+ If omitted or None, the entire source is used.
+ :param reducing_gap: Apply optimization by resizing the image
+ in two steps. First, reducing the image by integer times
+ using :py:meth:`~PIL.Image.Image.reduce`.
+ Second, resizing using regular resampling. The last step
+ changes size no less than by ``reducing_gap`` times.
+ ``reducing_gap`` may be None (no first step is performed)
+ or should be greater than 1.0. The bigger ``reducing_gap``,
+ the closer the result to the fair resampling.
+ The smaller ``reducing_gap``, the faster resizing.
+ With ``reducing_gap`` greater or equal to 3.0, the result is
+ indistinguishable from fair resampling in most cases.
+ The default value is None (no optimization).
+ :returns: An :py:class:`~PIL.Image.Image` object.
+ """
+
+ if resample is None:
+ type_special = ";" in self.mode
+ resample = Resampling.NEAREST if type_special else Resampling.BICUBIC
+ elif resample not in (
+ Resampling.NEAREST,
+ Resampling.BILINEAR,
+ Resampling.BICUBIC,
+ Resampling.LANCZOS,
+ Resampling.BOX,
+ Resampling.HAMMING,
+ ):
+ message = f"Unknown resampling filter ({resample})."
+
+ filters = [
+ f"{filter[1]} ({filter[0]})"
+ for filter in (
+ (Resampling.NEAREST, "Image.Resampling.NEAREST"),
+ (Resampling.LANCZOS, "Image.Resampling.LANCZOS"),
+ (Resampling.BILINEAR, "Image.Resampling.BILINEAR"),
+ (Resampling.BICUBIC, "Image.Resampling.BICUBIC"),
+ (Resampling.BOX, "Image.Resampling.BOX"),
+ (Resampling.HAMMING, "Image.Resampling.HAMMING"),
+ )
+ ]
+ raise ValueError(
+ message + " Use " + ", ".join(filters[:-1]) + " or " + filters[-1]
+ )
+
+ if reducing_gap is not None and reducing_gap < 1.0:
+ raise ValueError("reducing_gap must be 1.0 or greater")
+
+ size = tuple(size)
+
+ if box is None:
+ box = (0, 0) + self.size
+ else:
+ box = tuple(box)
+
+ if self.size == size and box == (0, 0) + self.size:
+ return self.copy()
+
+ if self.mode in ("1", "P"):
+ resample = Resampling.NEAREST
+
+ if self.mode in ["LA", "RGBA"] and resample != Resampling.NEAREST:
+ im = self.convert({"LA": "La", "RGBA": "RGBa"}[self.mode])
+ im = im.resize(size, resample, box)
+ return im.convert(self.mode)
+
+ self.load()
+
+ if reducing_gap is not None and resample != Resampling.NEAREST:
+ factor_x = int((box[2] - box[0]) / size[0] / reducing_gap) or 1
+ factor_y = int((box[3] - box[1]) / size[1] / reducing_gap) or 1
+ if factor_x > 1 or factor_y > 1:
+ reduce_box = self._get_safe_box(size, resample, box)
+ factor = (factor_x, factor_y)
+ if callable(self.reduce):
+ self = self.reduce(factor, box=reduce_box)
+ else:
+ self = Image.reduce(self, factor, box=reduce_box)
+ box = (
+ (box[0] - reduce_box[0]) / factor_x,
+ (box[1] - reduce_box[1]) / factor_y,
+ (box[2] - reduce_box[0]) / factor_x,
+ (box[3] - reduce_box[1]) / factor_y,
+ )
+
+ return self._new(self.im.resize(size, resample, box))
+
+ def reduce(self, factor, box=None):
+ """
+ Returns a copy of the image reduced ``factor`` times.
+ If the size of the image is not dividable by ``factor``,
+ the resulting size will be rounded up.
+
+ :param factor: A greater than 0 integer or tuple of two integers
+ for width and height separately.
+ :param box: An optional 4-tuple of ints providing
+ the source image region to be reduced.
+ The values must be within ``(0, 0, width, height)`` rectangle.
+ If omitted or ``None``, the entire source is used.
+ """
+ if not isinstance(factor, (list, tuple)):
+ factor = (factor, factor)
+
+ if box is None:
+ box = (0, 0) + self.size
+ else:
+ box = tuple(box)
+
+ if factor == (1, 1) and box == (0, 0) + self.size:
+ return self.copy()
+
+ if self.mode in ["LA", "RGBA"]:
+ im = self.convert({"LA": "La", "RGBA": "RGBa"}[self.mode])
+ im = im.reduce(factor, box)
+ return im.convert(self.mode)
+
+ self.load()
+
+ return self._new(self.im.reduce(factor, box))
+
+ def rotate(
+ self,
+ angle,
+ resample=Resampling.NEAREST,
+ expand=0,
+ center=None,
+ translate=None,
+ fillcolor=None,
+ ):
+ """
+ Returns a rotated copy of this image. This method returns a
+ copy of this image, rotated the given number of degrees counter
+ clockwise around its centre.
+
+ :param angle: In degrees counter clockwise.
+ :param resample: An optional resampling filter. This can be
+ one of :py:data:`PIL.Image.Resampling.NEAREST` (use nearest neighbour),
+ :py:data:`PIL.Image.BILINEAR` (linear interpolation in a 2x2
+ environment), or :py:data:`PIL.Image.Resampling.BICUBIC`
+ (cubic spline interpolation in a 4x4 environment).
+ If omitted, or if the image has mode "1" or "P", it is
+ set to :py:data:`PIL.Image.Resampling.NEAREST`. See :ref:`concept-filters`.
+ :param expand: Optional expansion flag. If true, expands the output
+ image to make it large enough to hold the entire rotated image.
+ If false or omitted, make the output image the same size as the
+ input image. Note that the expand flag assumes rotation around
+ the center and no translation.
+ :param center: Optional center of rotation (a 2-tuple). Origin is
+ the upper left corner. Default is the center of the image.
+ :param translate: An optional post-rotate translation (a 2-tuple).
+ :param fillcolor: An optional color for area outside the rotated image.
+ :returns: An :py:class:`~PIL.Image.Image` object.
+ """
+
+ angle = angle % 360.0
+
+ # Fast paths regardless of filter, as long as we're not
+ # translating or changing the center.
+ if not (center or translate):
+ if angle == 0:
+ return self.copy()
+ if angle == 180:
+ return self.transpose(Transpose.ROTATE_180)
+ if angle in (90, 270) and (expand or self.width == self.height):
+ return self.transpose(
+ Transpose.ROTATE_90 if angle == 90 else Transpose.ROTATE_270
+ )
+
+ # Calculate the affine matrix. Note that this is the reverse
+ # transformation (from destination image to source) because we
+ # want to interpolate the (discrete) destination pixel from
+ # the local area around the (floating) source pixel.
+
+ # The matrix we actually want (note that it operates from the right):
+ # (1, 0, tx) (1, 0, cx) ( cos a, sin a, 0) (1, 0, -cx)
+ # (0, 1, ty) * (0, 1, cy) * (-sin a, cos a, 0) * (0, 1, -cy)
+ # (0, 0, 1) (0, 0, 1) ( 0, 0, 1) (0, 0, 1)
+
+ # The reverse matrix is thus:
+ # (1, 0, cx) ( cos -a, sin -a, 0) (1, 0, -cx) (1, 0, -tx)
+ # (0, 1, cy) * (-sin -a, cos -a, 0) * (0, 1, -cy) * (0, 1, -ty)
+ # (0, 0, 1) ( 0, 0, 1) (0, 0, 1) (0, 0, 1)
+
+ # In any case, the final translation may be updated at the end to
+ # compensate for the expand flag.
+
+ w, h = self.size
+
+ if translate is None:
+ post_trans = (0, 0)
+ else:
+ post_trans = translate
+ if center is None:
+ # FIXME These should be rounded to ints?
+ rotn_center = (w / 2.0, h / 2.0)
+ else:
+ rotn_center = center
+
+ angle = -math.radians(angle)
+ matrix = [
+ round(math.cos(angle), 15),
+ round(math.sin(angle), 15),
+ 0.0,
+ round(-math.sin(angle), 15),
+ round(math.cos(angle), 15),
+ 0.0,
+ ]
+
+ def transform(x, y, matrix):
+ (a, b, c, d, e, f) = matrix
+ return a * x + b * y + c, d * x + e * y + f
+
+ matrix[2], matrix[5] = transform(
+ -rotn_center[0] - post_trans[0], -rotn_center[1] - post_trans[1], matrix
+ )
+ matrix[2] += rotn_center[0]
+ matrix[5] += rotn_center[1]
+
+ if expand:
+ # calculate output size
+ xx = []
+ yy = []
+ for x, y in ((0, 0), (w, 0), (w, h), (0, h)):
+ x, y = transform(x, y, matrix)
+ xx.append(x)
+ yy.append(y)
+ nw = math.ceil(max(xx)) - math.floor(min(xx))
+ nh = math.ceil(max(yy)) - math.floor(min(yy))
+
+ # We multiply a translation matrix from the right. Because of its
+ # special form, this is the same as taking the image of the
+ # translation vector as new translation vector.
+ matrix[2], matrix[5] = transform(-(nw - w) / 2.0, -(nh - h) / 2.0, matrix)
+ w, h = nw, nh
+
+ return self.transform(
+ (w, h), Transform.AFFINE, matrix, resample, fillcolor=fillcolor
+ )
+
+ def save(self, fp, format=None, **params):
+ """
+ Saves this image under the given filename. If no format is
+ specified, the format to use is determined from the filename
+ extension, if possible.
+
+ Keyword options can be used to provide additional instructions
+ to the writer. If a writer doesn't recognise an option, it is
+ silently ignored. The available options are described in the
+ :doc:`image format documentation
+ <../handbook/image-file-formats>` for each writer.
+
+ You can use a file object instead of a filename. In this case,
+ you must always specify the format. The file object must
+ implement the ``seek``, ``tell``, and ``write``
+ methods, and be opened in binary mode.
+
+ :param fp: A filename (string), pathlib.Path object or file object.
+ :param format: Optional format override. If omitted, the
+ format to use is determined from the filename extension.
+ If a file object was used instead of a filename, this
+ parameter should always be used.
+ :param params: Extra parameters to the image writer.
+ :returns: None
+ :exception ValueError: If the output format could not be determined
+ from the file name. Use the format option to solve this.
+ :exception OSError: If the file could not be written. The file
+ may have been created, and may contain partial data.
+ """
+
+ filename = ""
+ open_fp = False
+ if isinstance(fp, Path):
+ filename = str(fp)
+ open_fp = True
+ elif isPath(fp):
+ filename = fp
+ open_fp = True
+ elif fp == sys.stdout:
+ try:
+ fp = sys.stdout.buffer
+ except AttributeError:
+ pass
+ if not filename and hasattr(fp, "name") and isPath(fp.name):
+ # only set the name for metadata purposes
+ filename = fp.name
+
+ # may mutate self!
+ self._ensure_mutable()
+
+ save_all = params.pop("save_all", False)
+ self.encoderinfo = params
+ self.encoderconfig = ()
+
+ preinit()
+
+ ext = os.path.splitext(filename)[1].lower()
+
+ if not format:
+ if ext not in EXTENSION:
+ init()
+ try:
+ format = EXTENSION[ext]
+ except KeyError as e:
+ raise ValueError(f"unknown file extension: {ext}") from e
+
+ if format.upper() not in SAVE:
+ init()
+ if save_all:
+ save_handler = SAVE_ALL[format.upper()]
+ else:
+ save_handler = SAVE[format.upper()]
+
+ created = False
+ if open_fp:
+ created = not os.path.exists(filename)
+ if params.get("append", False):
+ # Open also for reading ("+"), because TIFF save_all
+ # writer needs to go back and edit the written data.
+ fp = builtins.open(filename, "r+b")
+ else:
+ fp = builtins.open(filename, "w+b")
+
+ try:
+ save_handler(self, fp, filename)
+ except Exception:
+ if open_fp:
+ fp.close()
+ if created:
+ try:
+ os.remove(filename)
+ except PermissionError:
+ pass
+ raise
+ if open_fp:
+ fp.close()
+
+ def seek(self, frame):
+ """
+ Seeks to the given frame in this sequence file. If you seek
+ beyond the end of the sequence, the method raises an
+ ``EOFError`` exception. When a sequence file is opened, the
+ library automatically seeks to frame 0.
+
+ See :py:meth:`~PIL.Image.Image.tell`.
+
+ If defined, :attr:`~PIL.Image.Image.n_frames` refers to the
+ number of available frames.
+
+ :param frame: Frame number, starting at 0.
+ :exception EOFError: If the call attempts to seek beyond the end
+ of the sequence.
+ """
+
+ # overridden by file handlers
+ if frame != 0:
+ raise EOFError
+
+ def show(self, title=None):
+ """
+ Displays this image. This method is mainly intended for debugging purposes.
+
+ This method calls :py:func:`PIL.ImageShow.show` internally. You can use
+ :py:func:`PIL.ImageShow.register` to override its default behaviour.
+
+ The image is first saved to a temporary file. By default, it will be in
+ PNG format.
+
+ On Unix, the image is then opened using the **display**, **eog** or
+ **xv** utility, depending on which one can be found.
+
+ On macOS, the image is opened with the native Preview application.
+
+ On Windows, the image is opened with the standard PNG display utility.
+
+ :param title: Optional title to use for the image window, where possible.
+ """
+
+ _show(self, title=title)
+
+ def split(self):
+ """
+ Split this image into individual bands. This method returns a
+ tuple of individual image bands from an image. For example,
+ splitting an "RGB" image creates three new images each
+ containing a copy of one of the original bands (red, green,
+ blue).
+
+ If you need only one band, :py:meth:`~PIL.Image.Image.getchannel`
+ method can be more convenient and faster.
+
+ :returns: A tuple containing bands.
+ """
+
+ self.load()
+ if self.im.bands == 1:
+ ims = [self.copy()]
+ else:
+ ims = map(self._new, self.im.split())
+ return tuple(ims)
+
+ def getchannel(self, channel):
+ """
+ Returns an image containing a single channel of the source image.
+
+ :param channel: What channel to return. Could be index
+ (0 for "R" channel of "RGB") or channel name
+ ("A" for alpha channel of "RGBA").
+ :returns: An image in "L" mode.
+
+ .. versionadded:: 4.3.0
+ """
+ self.load()
+
+ if isinstance(channel, str):
+ try:
+ channel = self.getbands().index(channel)
+ except ValueError as e:
+ raise ValueError(f'The image has no channel "{channel}"') from e
+
+ return self._new(self.im.getband(channel))
+
+ def tell(self):
+ """
+ Returns the current frame number. See :py:meth:`~PIL.Image.Image.seek`.
+
+ If defined, :attr:`~PIL.Image.Image.n_frames` refers to the
+ number of available frames.
+
+ :returns: Frame number, starting with 0.
+ """
+ return 0
+
+ def thumbnail(self, size, resample=Resampling.BICUBIC, reducing_gap=2.0):
+ """
+ Make this image into a thumbnail. This method modifies the
+ image to contain a thumbnail version of itself, no larger than
+ the given size. This method calculates an appropriate thumbnail
+ size to preserve the aspect of the image, calls the
+ :py:meth:`~PIL.Image.Image.draft` method to configure the file reader
+ (where applicable), and finally resizes the image.
+
+ Note that this function modifies the :py:class:`~PIL.Image.Image`
+ object in place. If you need to use the full resolution image as well,
+ apply this method to a :py:meth:`~PIL.Image.Image.copy` of the original
+ image.
+
+ :param size: Requested size.
+ :param resample: Optional resampling filter. This can be one
+ of :py:data:`PIL.Image.Resampling.NEAREST`,
+ :py:data:`PIL.Image.Resampling.BOX`,
+ :py:data:`PIL.Image.Resampling.BILINEAR`,
+ :py:data:`PIL.Image.Resampling.HAMMING`,
+ :py:data:`PIL.Image.Resampling.BICUBIC` or
+ :py:data:`PIL.Image.Resampling.LANCZOS`.
+ If omitted, it defaults to :py:data:`PIL.Image.Resampling.BICUBIC`.
+ (was :py:data:`PIL.Image.Resampling.NEAREST` prior to version 2.5.0).
+ See: :ref:`concept-filters`.
+ :param reducing_gap: Apply optimization by resizing the image
+ in two steps. First, reducing the image by integer times
+ using :py:meth:`~PIL.Image.Image.reduce` or
+ :py:meth:`~PIL.Image.Image.draft` for JPEG images.
+ Second, resizing using regular resampling. The last step
+ changes size no less than by ``reducing_gap`` times.
+ ``reducing_gap`` may be None (no first step is performed)
+ or should be greater than 1.0. The bigger ``reducing_gap``,
+ the closer the result to the fair resampling.
+ The smaller ``reducing_gap``, the faster resizing.
+ With ``reducing_gap`` greater or equal to 3.0, the result is
+ indistinguishable from fair resampling in most cases.
+ The default value is 2.0 (very close to fair resampling
+ while still being faster in many cases).
+ :returns: None
+ """
+
+ x, y = map(math.floor, size)
+ if x >= self.width and y >= self.height:
+ return
+
+ def round_aspect(number, key):
+ return max(min(math.floor(number), math.ceil(number), key=key), 1)
+
+ # preserve aspect ratio
+ aspect = self.width / self.height
+ if x / y >= aspect:
+ x = round_aspect(y * aspect, key=lambda n: abs(aspect - n / y))
+ else:
+ y = round_aspect(
+ x / aspect, key=lambda n: 0 if n == 0 else abs(aspect - x / n)
+ )
+ size = (x, y)
+
+ box = None
+ if reducing_gap is not None:
+ res = self.draft(None, (size[0] * reducing_gap, size[1] * reducing_gap))
+ if res is not None:
+ box = res[1]
+
+ if self.size != size:
+ im = self.resize(size, resample, box=box, reducing_gap=reducing_gap)
+
+ self.im = im.im
+ self._size = size
+ self.mode = self.im.mode
+
+ self.readonly = 0
+ self.pyaccess = None
+
+ # FIXME: the different transform methods need further explanation
+ # instead of bloating the method docs, add a separate chapter.
+ def transform(
+ self,
+ size,
+ method,
+ data=None,
+ resample=Resampling.NEAREST,
+ fill=1,
+ fillcolor=None,
+ ):
+ """
+ Transforms this image. This method creates a new image with the
+ given size, and the same mode as the original, and copies data
+ to the new image using the given transform.
+
+ :param size: The output size.
+ :param method: The transformation method. This is one of
+ :py:data:`PIL.Image.Transform.EXTENT` (cut out a rectangular subregion),
+ :py:data:`PIL.Image.Transform.AFFINE` (affine transform),
+ :py:data:`PIL.Image.Transform.PERSPECTIVE` (perspective transform),
+ :py:data:`PIL.Image.Transform.QUAD` (map a quadrilateral to a rectangle), or
+ :py:data:`PIL.Image.Transform.MESH` (map a number of source quadrilaterals
+ in one operation).
+
+ It may also be an :py:class:`~PIL.Image.ImageTransformHandler`
+ object::
+
+ class Example(Image.ImageTransformHandler):
+ def transform(self, size, data, resample, fill=1):
+ # Return result
+
+ It may also be an object with a ``method.getdata`` method
+ that returns a tuple supplying new ``method`` and ``data`` values::
+
+ class Example:
+ def getdata(self):
+ method = Image.Transform.EXTENT
+ data = (0, 0, 100, 100)
+ return method, data
+ :param data: Extra data to the transformation method.
+ :param resample: Optional resampling filter. It can be one of
+ :py:data:`PIL.Image.Resampling.NEAREST` (use nearest neighbour),
+ :py:data:`PIL.Image.Resampling.BILINEAR` (linear interpolation in a 2x2
+ environment), or :py:data:`PIL.Image.BICUBIC` (cubic spline
+ interpolation in a 4x4 environment). If omitted, or if the image
+ has mode "1" or "P", it is set to :py:data:`PIL.Image.Resampling.NEAREST`.
+ See: :ref:`concept-filters`.
+ :param fill: If ``method`` is an
+ :py:class:`~PIL.Image.ImageTransformHandler` object, this is one of
+ the arguments passed to it. Otherwise, it is unused.
+ :param fillcolor: Optional fill color for the area outside the
+ transform in the output image.
+ :returns: An :py:class:`~PIL.Image.Image` object.
+ """
+
+ if self.mode in ("LA", "RGBA") and resample != Resampling.NEAREST:
+ return (
+ self.convert({"LA": "La", "RGBA": "RGBa"}[self.mode])
+ .transform(size, method, data, resample, fill, fillcolor)
+ .convert(self.mode)
+ )
+
+ if isinstance(method, ImageTransformHandler):
+ return method.transform(size, self, resample=resample, fill=fill)
+
+ if hasattr(method, "getdata"):
+ # compatibility w. old-style transform objects
+ method, data = method.getdata()
+
+ if data is None:
+ raise ValueError("missing method data")
+
+ im = new(self.mode, size, fillcolor)
+ if self.mode == "P" and self.palette:
+ im.palette = self.palette.copy()
+ im.info = self.info.copy()
+ if method == Transform.MESH:
+ # list of quads
+ for box, quad in data:
+ im.__transformer(
+ box, self, Transform.QUAD, quad, resample, fillcolor is None
+ )
+ else:
+ im.__transformer(
+ (0, 0) + size, self, method, data, resample, fillcolor is None
+ )
+
+ return im
+
+ def __transformer(
+ self, box, image, method, data, resample=Resampling.NEAREST, fill=1
+ ):
+ w = box[2] - box[0]
+ h = box[3] - box[1]
+
+ if method == Transform.AFFINE:
+ data = data[0:6]
+
+ elif method == Transform.EXTENT:
+ # convert extent to an affine transform
+ x0, y0, x1, y1 = data
+ xs = (x1 - x0) / w
+ ys = (y1 - y0) / h
+ method = Transform.AFFINE
+ data = (xs, 0, x0, 0, ys, y0)
+
+ elif method == Transform.PERSPECTIVE:
+ data = data[0:8]
+
+ elif method == Transform.QUAD:
+ # quadrilateral warp. data specifies the four corners
+ # given as NW, SW, SE, and NE.
+ nw = data[0:2]
+ sw = data[2:4]
+ se = data[4:6]
+ ne = data[6:8]
+ x0, y0 = nw
+ As = 1.0 / w
+ At = 1.0 / h
+ data = (
+ x0,
+ (ne[0] - x0) * As,
+ (sw[0] - x0) * At,
+ (se[0] - sw[0] - ne[0] + x0) * As * At,
+ y0,
+ (ne[1] - y0) * As,
+ (sw[1] - y0) * At,
+ (se[1] - sw[1] - ne[1] + y0) * As * At,
+ )
+
+ else:
+ raise ValueError("unknown transformation method")
+
+ if resample not in (
+ Resampling.NEAREST,
+ Resampling.BILINEAR,
+ Resampling.BICUBIC,
+ ):
+ if resample in (Resampling.BOX, Resampling.HAMMING, Resampling.LANCZOS):
+ message = {
+ Resampling.BOX: "Image.Resampling.BOX",
+ Resampling.HAMMING: "Image.Resampling.HAMMING",
+ Resampling.LANCZOS: "Image.Resampling.LANCZOS",
+ }[resample] + f" ({resample}) cannot be used."
+ else:
+ message = f"Unknown resampling filter ({resample})."
+
+ filters = [
+ f"{filter[1]} ({filter[0]})"
+ for filter in (
+ (Resampling.NEAREST, "Image.Resampling.NEAREST"),
+ (Resampling.BILINEAR, "Image.Resampling.BILINEAR"),
+ (Resampling.BICUBIC, "Image.Resampling.BICUBIC"),
+ )
+ ]
+ raise ValueError(
+ message + " Use " + ", ".join(filters[:-1]) + " or " + filters[-1]
+ )
+
+ image.load()
+
+ self.load()
+
+ if image.mode in ("1", "P"):
+ resample = Resampling.NEAREST
+
+ self.im.transform2(box, image.im, method, data, resample, fill)
+
+ def transpose(self, method):
+ """
+ Transpose image (flip or rotate in 90 degree steps)
+
+ :param method: One of :py:data:`PIL.Image.Transpose.FLIP_LEFT_RIGHT`,
+ :py:data:`PIL.Image.Transpose.FLIP_TOP_BOTTOM`,
+ :py:data:`PIL.Image.Transpose.ROTATE_90`,
+ :py:data:`PIL.Image.Transpose.ROTATE_180`,
+ :py:data:`PIL.Image.Transpose.ROTATE_270`,
+ :py:data:`PIL.Image.Transpose.TRANSPOSE` or
+ :py:data:`PIL.Image.Transpose.TRANSVERSE`.
+ :returns: Returns a flipped or rotated copy of this image.
+ """
+
+ self.load()
+ return self._new(self.im.transpose(method))
+
+ def effect_spread(self, distance):
+ """
+ Randomly spread pixels in an image.
+
+ :param distance: Distance to spread pixels.
+ """
+ self.load()
+ return self._new(self.im.effect_spread(distance))
+
+ def toqimage(self):
+ """Returns a QImage copy of this image"""
+ from . import ImageQt
+
+ if not ImageQt.qt_is_installed:
+ raise ImportError("Qt bindings are not installed")
+ return ImageQt.toqimage(self)
+
+ def toqpixmap(self):
+ """Returns a QPixmap copy of this image"""
+ from . import ImageQt
+
+ if not ImageQt.qt_is_installed:
+ raise ImportError("Qt bindings are not installed")
+ return ImageQt.toqpixmap(self)
+
+
+# --------------------------------------------------------------------
+# Abstract handlers.
+
+
+class ImagePointHandler:
+ """
+ Used as a mixin by point transforms
+ (for use with :py:meth:`~PIL.Image.Image.point`)
+ """
+
+ pass
+
+
+class ImageTransformHandler:
+ """
+ Used as a mixin by geometry transforms
+ (for use with :py:meth:`~PIL.Image.Image.transform`)
+ """
+
+ pass
+
+
+# --------------------------------------------------------------------
+# Factories
+
+#
+# Debugging
+
+
+def _wedge():
+ """Create greyscale wedge (for debugging only)"""
+
+ return Image()._new(core.wedge("L"))
+
+
+def _check_size(size):
+ """
+ Common check to enforce type and sanity check on size tuples
+
+ :param size: Should be a 2 tuple of (width, height)
+ :returns: True, or raises a ValueError
+ """
+
+ if not isinstance(size, (list, tuple)):
+ raise ValueError("Size must be a tuple")
+ if len(size) != 2:
+ raise ValueError("Size must be a tuple of length 2")
+ if size[0] < 0 or size[1] < 0:
+ raise ValueError("Width and height must be >= 0")
+
+ return True
+
+
+def new(mode, size, color=0):
+ """
+ Creates a new image with the given mode and size.
+
+ :param mode: The mode to use for the new image. See:
+ :ref:`concept-modes`.
+ :param size: A 2-tuple, containing (width, height) in pixels.
+ :param color: What color to use for the image. Default is black.
+ If given, this should be a single integer or floating point value
+ for single-band modes, and a tuple for multi-band modes (one value
+ per band). When creating RGB images, you can also use color
+ strings as supported by the ImageColor module. If the color is
+ None, the image is not initialised.
+ :returns: An :py:class:`~PIL.Image.Image` object.
+ """
+
+ _check_size(size)
+
+ if color is None:
+ # don't initialize
+ return Image()._new(core.new(mode, size))
+
+ if isinstance(color, str):
+ # css3-style specifier
+
+ from . import ImageColor
+
+ color = ImageColor.getcolor(color, mode)
+
+ im = Image()
+ if mode == "P" and isinstance(color, (list, tuple)) and len(color) in [3, 4]:
+ # RGB or RGBA value for a P image
+ from . import ImagePalette
+
+ im.palette = ImagePalette.ImagePalette()
+ color = im.palette.getcolor(color)
+ return im._new(core.fill(mode, size, color))
+
+
+def frombytes(mode, size, data, decoder_name="raw", *args):
+ """
+ Creates a copy of an image memory from pixel data in a buffer.
+
+ In its simplest form, this function takes three arguments
+ (mode, size, and unpacked pixel data).
+
+ You can also use any pixel decoder supported by PIL. For more
+ information on available decoders, see the section
+ :ref:`Writing Your Own File Codec `.
+
+ Note that this function decodes pixel data only, not entire images.
+ If you have an entire image in a string, wrap it in a
+ :py:class:`~io.BytesIO` object, and use :py:func:`~PIL.Image.open` to load
+ it.
+
+ :param mode: The image mode. See: :ref:`concept-modes`.
+ :param size: The image size.
+ :param data: A byte buffer containing raw data for the given mode.
+ :param decoder_name: What decoder to use.
+ :param args: Additional parameters for the given decoder.
+ :returns: An :py:class:`~PIL.Image.Image` object.
+ """
+
+ _check_size(size)
+
+ # may pass tuple instead of argument list
+ if len(args) == 1 and isinstance(args[0], tuple):
+ args = args[0]
+
+ if decoder_name == "raw" and args == ():
+ args = mode
+
+ im = new(mode, size)
+ im.frombytes(data, decoder_name, args)
+ return im
+
+
+def frombuffer(mode, size, data, decoder_name="raw", *args):
+ """
+ Creates an image memory referencing pixel data in a byte buffer.
+
+ This function is similar to :py:func:`~PIL.Image.frombytes`, but uses data
+ in the byte buffer, where possible. This means that changes to the
+ original buffer object are reflected in this image). Not all modes can
+ share memory; supported modes include "L", "RGBX", "RGBA", and "CMYK".
+
+ Note that this function decodes pixel data only, not entire images.
+ If you have an entire image file in a string, wrap it in a
+ :py:class:`~io.BytesIO` object, and use :py:func:`~PIL.Image.open` to load it.
+
+ In the current version, the default parameters used for the "raw" decoder
+ differs from that used for :py:func:`~PIL.Image.frombytes`. This is a
+ bug, and will probably be fixed in a future release. The current release
+ issues a warning if you do this; to disable the warning, you should provide
+ the full set of parameters. See below for details.
+
+ :param mode: The image mode. See: :ref:`concept-modes`.
+ :param size: The image size.
+ :param data: A bytes or other buffer object containing raw
+ data for the given mode.
+ :param decoder_name: What decoder to use.
+ :param args: Additional parameters for the given decoder. For the
+ default encoder ("raw"), it's recommended that you provide the
+ full set of parameters::
+
+ frombuffer(mode, size, data, "raw", mode, 0, 1)
+
+ :returns: An :py:class:`~PIL.Image.Image` object.
+
+ .. versionadded:: 1.1.4
+ """
+
+ _check_size(size)
+
+ # may pass tuple instead of argument list
+ if len(args) == 1 and isinstance(args[0], tuple):
+ args = args[0]
+
+ if decoder_name == "raw":
+ if args == ():
+ args = mode, 0, 1
+ if args[0] in _MAPMODES:
+ im = new(mode, (1, 1))
+ im = im._new(core.map_buffer(data, size, decoder_name, 0, args))
+ im.readonly = 1
+ return im
+
+ return frombytes(mode, size, data, decoder_name, args)
+
+
+def fromarray(obj, mode=None):
+ """
+ Creates an image memory from an object exporting the array interface
+ (using the buffer protocol).
+
+ If ``obj`` is not contiguous, then the ``tobytes`` method is called
+ and :py:func:`~PIL.Image.frombuffer` is used.
+
+ If you have an image in NumPy::
+
+ from PIL import Image
+ import numpy as np
+ im = Image.open("hopper.jpg")
+ a = np.asarray(im)
+
+ Then this can be used to convert it to a Pillow image::
+
+ im = Image.fromarray(a)
+
+ :param obj: Object with array interface
+ :param mode: Optional mode to use when reading ``obj``. Will be determined from
+ type if ``None``.
+
+ This will not be used to convert the data after reading, but will be used to
+ change how the data is read::
+
+ from PIL import Image
+ import numpy as np
+ a = np.full((1, 1), 300)
+ im = Image.fromarray(a, mode="L")
+ im.getpixel((0, 0)) # 44
+ im = Image.fromarray(a, mode="RGB")
+ im.getpixel((0, 0)) # (44, 1, 0)
+
+ See: :ref:`concept-modes` for general information about modes.
+ :returns: An image object.
+
+ .. versionadded:: 1.1.6
+ """
+ arr = obj.__array_interface__
+ shape = arr["shape"]
+ ndim = len(shape)
+ strides = arr.get("strides", None)
+ if mode is None:
+ try:
+ typekey = (1, 1) + shape[2:], arr["typestr"]
+ except KeyError as e:
+ raise TypeError("Cannot handle this data type") from e
+ try:
+ mode, rawmode = _fromarray_typemap[typekey]
+ except KeyError as e:
+ raise TypeError("Cannot handle this data type: %s, %s" % typekey) from e
+ else:
+ rawmode = mode
+ if mode in ["1", "L", "I", "P", "F"]:
+ ndmax = 2
+ elif mode == "RGB":
+ ndmax = 3
+ else:
+ ndmax = 4
+ if ndim > ndmax:
+ raise ValueError(f"Too many dimensions: {ndim} > {ndmax}.")
+
+ size = 1 if ndim == 1 else shape[1], shape[0]
+ if strides is not None:
+ if hasattr(obj, "tobytes"):
+ obj = obj.tobytes()
+ else:
+ obj = obj.tostring()
+
+ return frombuffer(mode, size, obj, "raw", rawmode, 0, 1)
+
+
+def fromqimage(im):
+ """Creates an image instance from a QImage image"""
+ from . import ImageQt
+
+ if not ImageQt.qt_is_installed:
+ raise ImportError("Qt bindings are not installed")
+ return ImageQt.fromqimage(im)
+
+
+def fromqpixmap(im):
+ """Creates an image instance from a QPixmap image"""
+ from . import ImageQt
+
+ if not ImageQt.qt_is_installed:
+ raise ImportError("Qt bindings are not installed")
+ return ImageQt.fromqpixmap(im)
+
+
+_fromarray_typemap = {
+ # (shape, typestr) => mode, rawmode
+ # first two members of shape are set to one
+ ((1, 1), "|b1"): ("1", "1;8"),
+ ((1, 1), "|u1"): ("L", "L"),
+ ((1, 1), "|i1"): ("I", "I;8"),
+ ((1, 1), "u2"): ("I", "I;16B"),
+ ((1, 1), "i2"): ("I", "I;16BS"),
+ ((1, 1), "u4"): ("I", "I;32B"),
+ ((1, 1), "i4"): ("I", "I;32BS"),
+ ((1, 1), "f4"): ("F", "F;32BF"),
+ ((1, 1), "f8"): ("F", "F;64BF"),
+ ((1, 1, 2), "|u1"): ("LA", "LA"),
+ ((1, 1, 3), "|u1"): ("RGB", "RGB"),
+ ((1, 1, 4), "|u1"): ("RGBA", "RGBA"),
+}
+
+# shortcuts
+_fromarray_typemap[((1, 1), _ENDIAN + "i4")] = ("I", "I")
+_fromarray_typemap[((1, 1), _ENDIAN + "f4")] = ("F", "F")
+
+
+def _decompression_bomb_check(size):
+ if MAX_IMAGE_PIXELS is None:
+ return
+
+ pixels = size[0] * size[1]
+
+ if pixels > 2 * MAX_IMAGE_PIXELS:
+ raise DecompressionBombError(
+ f"Image size ({pixels} pixels) exceeds limit of {2 * MAX_IMAGE_PIXELS} "
+ "pixels, could be decompression bomb DOS attack."
+ )
+
+ if pixels > MAX_IMAGE_PIXELS:
+ warnings.warn(
+ f"Image size ({pixels} pixels) exceeds limit of {MAX_IMAGE_PIXELS} pixels, "
+ "could be decompression bomb DOS attack.",
+ DecompressionBombWarning,
+ )
+
+
+def open(fp, mode="r", formats=None):
+ """
+ Opens and identifies the given image file.
+
+ This is a lazy operation; this function identifies the file, but
+ the file remains open and the actual image data is not read from
+ the file until you try to process the data (or call the
+ :py:meth:`~PIL.Image.Image.load` method). See
+ :py:func:`~PIL.Image.new`. See :ref:`file-handling`.
+
+ :param fp: A filename (string), pathlib.Path object or a file object.
+ The file object must implement ``file.read``,
+ ``file.seek``, and ``file.tell`` methods,
+ and be opened in binary mode.
+ :param mode: The mode. If given, this argument must be "r".
+ :param formats: A list or tuple of formats to attempt to load the file in.
+ This can be used to restrict the set of formats checked.
+ Pass ``None`` to try all supported formats. You can print the set of
+ available formats by running ``python3 -m PIL`` or using
+ the :py:func:`PIL.features.pilinfo` function.
+ :returns: An :py:class:`~PIL.Image.Image` object.
+ :exception FileNotFoundError: If the file cannot be found.
+ :exception PIL.UnidentifiedImageError: If the image cannot be opened and
+ identified.
+ :exception ValueError: If the ``mode`` is not "r", or if a ``StringIO``
+ instance is used for ``fp``.
+ :exception TypeError: If ``formats`` is not ``None``, a list or a tuple.
+ """
+
+ if mode != "r":
+ raise ValueError(f"bad mode {repr(mode)}")
+ elif isinstance(fp, io.StringIO):
+ raise ValueError(
+ "StringIO cannot be used to open an image. "
+ "Binary data must be used instead."
+ )
+
+ if formats is None:
+ formats = ID
+ elif not isinstance(formats, (list, tuple)):
+ raise TypeError("formats must be a list or tuple")
+
+ exclusive_fp = False
+ filename = ""
+ if isinstance(fp, Path):
+ filename = str(fp.resolve())
+ elif isPath(fp):
+ filename = fp
+
+ if filename:
+ fp = builtins.open(filename, "rb")
+ exclusive_fp = True
+
+ try:
+ fp.seek(0)
+ except (AttributeError, io.UnsupportedOperation):
+ fp = io.BytesIO(fp.read())
+ exclusive_fp = True
+
+ prefix = fp.read(16)
+
+ preinit()
+
+ accept_warnings = []
+
+ def _open_core(fp, filename, prefix, formats):
+ for i in formats:
+ i = i.upper()
+ if i not in OPEN:
+ init()
+ try:
+ factory, accept = OPEN[i]
+ result = not accept or accept(prefix)
+ if type(result) in [str, bytes]:
+ accept_warnings.append(result)
+ elif result:
+ fp.seek(0)
+ im = factory(fp, filename)
+ _decompression_bomb_check(im.size)
+ return im
+ except (SyntaxError, IndexError, TypeError, struct.error):
+ # Leave disabled by default, spams the logs with image
+ # opening failures that are entirely expected.
+ # logger.debug("", exc_info=True)
+ continue
+ except BaseException:
+ if exclusive_fp:
+ fp.close()
+ raise
+ return None
+
+ im = _open_core(fp, filename, prefix, formats)
+
+ if im is None:
+ if init():
+ im = _open_core(fp, filename, prefix, formats)
+
+ if im:
+ im._exclusive_fp = exclusive_fp
+ return im
+
+ if exclusive_fp:
+ fp.close()
+ for message in accept_warnings:
+ warnings.warn(message)
+ raise UnidentifiedImageError(
+ "cannot identify image file %r" % (filename if filename else fp)
+ )
+
+
+#
+# Image processing.
+
+
+def alpha_composite(im1, im2):
+ """
+ Alpha composite im2 over im1.
+
+ :param im1: The first image. Must have mode RGBA.
+ :param im2: The second image. Must have mode RGBA, and the same size as
+ the first image.
+ :returns: An :py:class:`~PIL.Image.Image` object.
+ """
+
+ im1.load()
+ im2.load()
+ return im1._new(core.alpha_composite(im1.im, im2.im))
+
+
+def blend(im1, im2, alpha):
+ """
+ Creates a new image by interpolating between two input images, using
+ a constant alpha::
+
+ out = image1 * (1.0 - alpha) + image2 * alpha
+
+ :param im1: The first image.
+ :param im2: The second image. Must have the same mode and size as
+ the first image.
+ :param alpha: The interpolation alpha factor. If alpha is 0.0, a
+ copy of the first image is returned. If alpha is 1.0, a copy of
+ the second image is returned. There are no restrictions on the
+ alpha value. If necessary, the result is clipped to fit into
+ the allowed output range.
+ :returns: An :py:class:`~PIL.Image.Image` object.
+ """
+
+ im1.load()
+ im2.load()
+ return im1._new(core.blend(im1.im, im2.im, alpha))
+
+
+def composite(image1, image2, mask):
+ """
+ Create composite image by blending images using a transparency mask.
+
+ :param image1: The first image.
+ :param image2: The second image. Must have the same mode and
+ size as the first image.
+ :param mask: A mask image. This image can have mode
+ "1", "L", or "RGBA", and must have the same size as the
+ other two images.
+ """
+
+ image = image2.copy()
+ image.paste(image1, None, mask)
+ return image
+
+
+def eval(image, *args):
+ """
+ Applies the function (which should take one argument) to each pixel
+ in the given image. If the image has more than one band, the same
+ function is applied to each band. Note that the function is
+ evaluated once for each possible pixel value, so you cannot use
+ random components or other generators.
+
+ :param image: The input image.
+ :param function: A function object, taking one integer argument.
+ :returns: An :py:class:`~PIL.Image.Image` object.
+ """
+
+ return image.point(args[0])
+
+
+def merge(mode, bands):
+ """
+ Merge a set of single band images into a new multiband image.
+
+ :param mode: The mode to use for the output image. See:
+ :ref:`concept-modes`.
+ :param bands: A sequence containing one single-band image for
+ each band in the output image. All bands must have the
+ same size.
+ :returns: An :py:class:`~PIL.Image.Image` object.
+ """
+
+ if getmodebands(mode) != len(bands) or "*" in mode:
+ raise ValueError("wrong number of bands")
+ for band in bands[1:]:
+ if band.mode != getmodetype(mode):
+ raise ValueError("mode mismatch")
+ if band.size != bands[0].size:
+ raise ValueError("size mismatch")
+ for band in bands:
+ band.load()
+ return bands[0]._new(core.merge(mode, *[b.im for b in bands]))
+
+
+# --------------------------------------------------------------------
+# Plugin registry
+
+
+def register_open(id, factory, accept=None):
+ """
+ Register an image file plugin. This function should not be used
+ in application code.
+
+ :param id: An image format identifier.
+ :param factory: An image file factory method.
+ :param accept: An optional function that can be used to quickly
+ reject images having another format.
+ """
+ id = id.upper()
+ ID.append(id)
+ OPEN[id] = factory, accept
+
+
+def register_mime(id, mimetype):
+ """
+ Registers an image MIME type. This function should not be used
+ in application code.
+
+ :param id: An image format identifier.
+ :param mimetype: The image MIME type for this format.
+ """
+ MIME[id.upper()] = mimetype
+
+
+def register_save(id, driver):
+ """
+ Registers an image save function. This function should not be
+ used in application code.
+
+ :param id: An image format identifier.
+ :param driver: A function to save images in this format.
+ """
+ SAVE[id.upper()] = driver
+
+
+def register_save_all(id, driver):
+ """
+ Registers an image function to save all the frames
+ of a multiframe format. This function should not be
+ used in application code.
+
+ :param id: An image format identifier.
+ :param driver: A function to save images in this format.
+ """
+ SAVE_ALL[id.upper()] = driver
+
+
+def register_extension(id, extension):
+ """
+ Registers an image extension. This function should not be
+ used in application code.
+
+ :param id: An image format identifier.
+ :param extension: An extension used for this format.
+ """
+ EXTENSION[extension.lower()] = id.upper()
+
+
+def register_extensions(id, extensions):
+ """
+ Registers image extensions. This function should not be
+ used in application code.
+
+ :param id: An image format identifier.
+ :param extensions: A list of extensions used for this format.
+ """
+ for extension in extensions:
+ register_extension(id, extension)
+
+
+def registered_extensions():
+ """
+ Returns a dictionary containing all file extensions belonging
+ to registered plugins
+ """
+ if not EXTENSION:
+ init()
+ return EXTENSION
+
+
+def register_decoder(name, decoder):
+ """
+ Registers an image decoder. This function should not be
+ used in application code.
+
+ :param name: The name of the decoder
+ :param decoder: A callable(mode, args) that returns an
+ ImageFile.PyDecoder object
+
+ .. versionadded:: 4.1.0
+ """
+ DECODERS[name] = decoder
+
+
+def register_encoder(name, encoder):
+ """
+ Registers an image encoder. This function should not be
+ used in application code.
+
+ :param name: The name of the encoder
+ :param encoder: A callable(mode, args) that returns an
+ ImageFile.PyEncoder object
+
+ .. versionadded:: 4.1.0
+ """
+ ENCODERS[name] = encoder
+
+
+# --------------------------------------------------------------------
+# Simple display support.
+
+
+def _show(image, **options):
+ from . import ImageShow
+
+ ImageShow.show(image, **options)
+
+
+# --------------------------------------------------------------------
+# Effects
+
+
+def effect_mandelbrot(size, extent, quality):
+ """
+ Generate a Mandelbrot set covering the given extent.
+
+ :param size: The requested size in pixels, as a 2-tuple:
+ (width, height).
+ :param extent: The extent to cover, as a 4-tuple:
+ (x0, y0, x1, y2).
+ :param quality: Quality.
+ """
+ return Image()._new(core.effect_mandelbrot(size, extent, quality))
+
+
+def effect_noise(size, sigma):
+ """
+ Generate Gaussian noise centered around 128.
+
+ :param size: The requested size in pixels, as a 2-tuple:
+ (width, height).
+ :param sigma: Standard deviation of noise.
+ """
+ return Image()._new(core.effect_noise(size, sigma))
+
+
+def linear_gradient(mode):
+ """
+ Generate 256x256 linear gradient from black to white, top to bottom.
+
+ :param mode: Input mode.
+ """
+ return Image()._new(core.linear_gradient(mode))
+
+
+def radial_gradient(mode):
+ """
+ Generate 256x256 radial gradient from black to white, centre to edge.
+
+ :param mode: Input mode.
+ """
+ return Image()._new(core.radial_gradient(mode))
+
+
+# --------------------------------------------------------------------
+# Resources
+
+
+def _apply_env_variables(env=None):
+ if env is None:
+ env = os.environ
+
+ for var_name, setter in [
+ ("PILLOW_ALIGNMENT", core.set_alignment),
+ ("PILLOW_BLOCK_SIZE", core.set_block_size),
+ ("PILLOW_BLOCKS_MAX", core.set_blocks_max),
+ ]:
+ if var_name not in env:
+ continue
+
+ var = env[var_name].lower()
+
+ units = 1
+ for postfix, mul in [("k", 1024), ("m", 1024 * 1024)]:
+ if var.endswith(postfix):
+ units = mul
+ var = var[: -len(postfix)]
+
+ try:
+ var = int(var) * units
+ except ValueError:
+ warnings.warn(f"{var_name} is not int")
+ continue
+
+ try:
+ setter(var)
+ except ValueError as e:
+ warnings.warn(f"{var_name}: {e}")
+
+
+_apply_env_variables()
+atexit.register(core.clear_cache)
+
+
+class Exif(MutableMapping):
+ endian = None
+ bigtiff = False
+
+ def __init__(self):
+ self._data = {}
+ self._ifds = {}
+ self._info = None
+ self._loaded_exif = None
+
+ def _fixup(self, value):
+ try:
+ if len(value) == 1 and isinstance(value, tuple):
+ return value[0]
+ except Exception:
+ pass
+ return value
+
+ def _fixup_dict(self, src_dict):
+ # Helper function
+ # returns a dict with any single item tuples/lists as individual values
+ return {k: self._fixup(v) for k, v in src_dict.items()}
+
+ def _get_ifd_dict(self, offset):
+ try:
+ # an offset pointer to the location of the nested embedded IFD.
+ # It should be a long, but may be corrupted.
+ self.fp.seek(offset)
+ except (KeyError, TypeError):
+ pass
+ else:
+ from . import TiffImagePlugin
+
+ info = TiffImagePlugin.ImageFileDirectory_v2(self.head)
+ info.load(self.fp)
+ return self._fixup_dict(info)
+
+ def _get_head(self):
+ version = b"\x2B" if self.bigtiff else b"\x2A"
+ if self.endian == "<":
+ head = b"II" + version + b"\x00" + o32le(8)
+ else:
+ head = b"MM\x00" + version + o32be(8)
+ if self.bigtiff:
+ head += o32le(8) if self.endian == "<" else o32be(8)
+ head += b"\x00\x00\x00\x00"
+ return head
+
+ def load(self, data):
+ # Extract EXIF information. This is highly experimental,
+ # and is likely to be replaced with something better in a future
+ # version.
+
+ # The EXIF record consists of a TIFF file embedded in a JPEG
+ # application marker (!).
+ if data == self._loaded_exif:
+ return
+ self._loaded_exif = data
+ self._data.clear()
+ self._ifds.clear()
+ if data and data.startswith(b"Exif\x00\x00"):
+ data = data[6:]
+ if not data:
+ self._info = None
+ return
+
+ self.fp = io.BytesIO(data)
+ self.head = self.fp.read(8)
+ # process dictionary
+ from . import TiffImagePlugin
+
+ self._info = TiffImagePlugin.ImageFileDirectory_v2(self.head)
+ self.endian = self._info._endian
+ self.fp.seek(self._info.next)
+ self._info.load(self.fp)
+
+ def load_from_fp(self, fp, offset=None):
+ self._loaded_exif = None
+ self._data.clear()
+ self._ifds.clear()
+
+ # process dictionary
+ from . import TiffImagePlugin
+
+ self.fp = fp
+ if offset is not None:
+ self.head = self._get_head()
+ else:
+ self.head = self.fp.read(8)
+ self._info = TiffImagePlugin.ImageFileDirectory_v2(self.head)
+ if self.endian is None:
+ self.endian = self._info._endian
+ if offset is None:
+ offset = self._info.next
+ self.fp.seek(offset)
+ self._info.load(self.fp)
+
+ def _get_merged_dict(self):
+ merged_dict = dict(self)
+
+ # get EXIF extension
+ if 0x8769 in self:
+ ifd = self._get_ifd_dict(self[0x8769])
+ if ifd:
+ merged_dict.update(ifd)
+
+ # GPS
+ if 0x8825 in self:
+ merged_dict[0x8825] = self._get_ifd_dict(self[0x8825])
+
+ return merged_dict
+
+ def tobytes(self, offset=8):
+ from . import TiffImagePlugin
+
+ head = self._get_head()
+ ifd = TiffImagePlugin.ImageFileDirectory_v2(ifh=head)
+ for tag, value in self.items():
+ if tag in [0x8769, 0x8225, 0x8825] and not isinstance(value, dict):
+ value = self.get_ifd(tag)
+ if (
+ tag == 0x8769
+ and 0xA005 in value
+ and not isinstance(value[0xA005], dict)
+ ):
+ value = value.copy()
+ value[0xA005] = self.get_ifd(0xA005)
+ ifd[tag] = value
+ return b"Exif\x00\x00" + head + ifd.tobytes(offset)
+
+ def get_ifd(self, tag):
+ if tag not in self._ifds:
+ if tag in [0x8769, 0x8825]:
+ # exif, gpsinfo
+ if tag in self:
+ self._ifds[tag] = self._get_ifd_dict(self[tag])
+ elif tag in [0xA005, 0x927C]:
+ # interop, makernote
+ if 0x8769 not in self._ifds:
+ self.get_ifd(0x8769)
+ tag_data = self._ifds[0x8769][tag]
+ if tag == 0x927C:
+ # makernote
+ from .TiffImagePlugin import ImageFileDirectory_v2
+
+ if tag_data[:8] == b"FUJIFILM":
+ ifd_offset = i32le(tag_data, 8)
+ ifd_data = tag_data[ifd_offset:]
+
+ makernote = {}
+ for i in range(0, struct.unpack(" 4:
+ (offset,) = struct.unpack("H", tag_data[:2])[0]):
+ ifd_tag, typ, count, data = struct.unpack(
+ ">HHL4s", tag_data[i * 12 + 2 : (i + 1) * 12 + 2]
+ )
+ if ifd_tag == 0x1101:
+ # CameraInfo
+ (offset,) = struct.unpack(">L", data)
+ self.fp.seek(offset)
+
+ camerainfo = {"ModelID": self.fp.read(4)}
+
+ self.fp.read(4)
+ # Seconds since 2000
+ camerainfo["TimeStamp"] = i32le(self.fp.read(12))
+
+ self.fp.read(4)
+ camerainfo["InternalSerialNumber"] = self.fp.read(4)
+
+ self.fp.read(12)
+ parallax = self.fp.read(4)
+ handler = ImageFileDirectory_v2._load_dispatch[
+ TiffTags.FLOAT
+ ][1]
+ camerainfo["Parallax"] = handler(
+ ImageFileDirectory_v2(), parallax, False
+ )
+
+ self.fp.read(4)
+ camerainfo["Category"] = self.fp.read(2)
+
+ makernote = {0x1101: dict(self._fixup_dict(camerainfo))}
+ self._ifds[tag] = makernote
+ else:
+ # interop
+ self._ifds[tag] = self._get_ifd_dict(tag_data)
+ return self._ifds.get(tag, {})
+
+ def __str__(self):
+ if self._info is not None:
+ # Load all keys into self._data
+ for tag in self._info.keys():
+ self[tag]
+
+ return str(self._data)
+
+ def __len__(self):
+ keys = set(self._data)
+ if self._info is not None:
+ keys.update(self._info)
+ return len(keys)
+
+ def __getitem__(self, tag):
+ if self._info is not None and tag not in self._data and tag in self._info:
+ self._data[tag] = self._fixup(self._info[tag])
+ del self._info[tag]
+ return self._data[tag]
+
+ def __contains__(self, tag):
+ return tag in self._data or (self._info is not None and tag in self._info)
+
+ def __setitem__(self, tag, value):
+ if self._info is not None and tag in self._info:
+ del self._info[tag]
+ self._data[tag] = value
+
+ def __delitem__(self, tag):
+ if self._info is not None and tag in self._info:
+ del self._info[tag]
+ else:
+ del self._data[tag]
+
+ def __iter__(self):
+ keys = set(self._data)
+ if self._info is not None:
+ keys.update(self._info)
+ return iter(keys)
diff --git a/venv/Lib/site-packages/PIL/ImageChops.py b/venv/Lib/site-packages/PIL/ImageChops.py
new file mode 100644
index 0000000..61d3a29
--- /dev/null
+++ b/venv/Lib/site-packages/PIL/ImageChops.py
@@ -0,0 +1,328 @@
+#
+# The Python Imaging Library.
+# $Id$
+#
+# standard channel operations
+#
+# History:
+# 1996-03-24 fl Created
+# 1996-08-13 fl Added logical operations (for "1" images)
+# 2000-10-12 fl Added offset method (from Image.py)
+#
+# Copyright (c) 1997-2000 by Secret Labs AB
+# Copyright (c) 1996-2000 by Fredrik Lundh
+#
+# See the README file for information on usage and redistribution.
+#
+
+from . import Image
+
+
+def constant(image, value):
+ """Fill a channel with a given grey level.
+
+ :rtype: :py:class:`~PIL.Image.Image`
+ """
+
+ return Image.new("L", image.size, value)
+
+
+def duplicate(image):
+ """Copy a channel. Alias for :py:meth:`PIL.Image.Image.copy`.
+
+ :rtype: :py:class:`~PIL.Image.Image`
+ """
+
+ return image.copy()
+
+
+def invert(image):
+ """
+ Invert an image (channel).
+
+ .. code-block:: python
+
+ out = MAX - image
+
+ :rtype: :py:class:`~PIL.Image.Image`
+ """
+
+ image.load()
+ return image._new(image.im.chop_invert())
+
+
+def lighter(image1, image2):
+ """
+ Compares the two images, pixel by pixel, and returns a new image containing
+ the lighter values.
+
+ .. code-block:: python
+
+ out = max(image1, image2)
+
+ :rtype: :py:class:`~PIL.Image.Image`
+ """
+
+ image1.load()
+ image2.load()
+ return image1._new(image1.im.chop_lighter(image2.im))
+
+
+def darker(image1, image2):
+ """
+ Compares the two images, pixel by pixel, and returns a new image containing
+ the darker values.
+
+ .. code-block:: python
+
+ out = min(image1, image2)
+
+ :rtype: :py:class:`~PIL.Image.Image`
+ """
+
+ image1.load()
+ image2.load()
+ return image1._new(image1.im.chop_darker(image2.im))
+
+
+def difference(image1, image2):
+ """
+ Returns the absolute value of the pixel-by-pixel difference between the two
+ images.
+
+ .. code-block:: python
+
+ out = abs(image1 - image2)
+
+ :rtype: :py:class:`~PIL.Image.Image`
+ """
+
+ image1.load()
+ image2.load()
+ return image1._new(image1.im.chop_difference(image2.im))
+
+
+def multiply(image1, image2):
+ """
+ Superimposes two images on top of each other.
+
+ If you multiply an image with a solid black image, the result is black. If
+ you multiply with a solid white image, the image is unaffected.
+
+ .. code-block:: python
+
+ out = image1 * image2 / MAX
+
+ :rtype: :py:class:`~PIL.Image.Image`
+ """
+
+ image1.load()
+ image2.load()
+ return image1._new(image1.im.chop_multiply(image2.im))
+
+
+def screen(image1, image2):
+ """
+ Superimposes two inverted images on top of each other.
+
+ .. code-block:: python
+
+ out = MAX - ((MAX - image1) * (MAX - image2) / MAX)
+
+ :rtype: :py:class:`~PIL.Image.Image`
+ """
+
+ image1.load()
+ image2.load()
+ return image1._new(image1.im.chop_screen(image2.im))
+
+
+def soft_light(image1, image2):
+ """
+ Superimposes two images on top of each other using the Soft Light algorithm
+
+ :rtype: :py:class:`~PIL.Image.Image`
+ """
+
+ image1.load()
+ image2.load()
+ return image1._new(image1.im.chop_soft_light(image2.im))
+
+
+def hard_light(image1, image2):
+ """
+ Superimposes two images on top of each other using the Hard Light algorithm
+
+ :rtype: :py:class:`~PIL.Image.Image`
+ """
+
+ image1.load()
+ image2.load()
+ return image1._new(image1.im.chop_hard_light(image2.im))
+
+
+def overlay(image1, image2):
+ """
+ Superimposes two images on top of each other using the Overlay algorithm
+
+ :rtype: :py:class:`~PIL.Image.Image`
+ """
+
+ image1.load()
+ image2.load()
+ return image1._new(image1.im.chop_overlay(image2.im))
+
+
+def add(image1, image2, scale=1.0, offset=0):
+ """
+ Adds two images, dividing the result by scale and adding the
+ offset. If omitted, scale defaults to 1.0, and offset to 0.0.
+
+ .. code-block:: python
+
+ out = ((image1 + image2) / scale + offset)
+
+ :rtype: :py:class:`~PIL.Image.Image`
+ """
+
+ image1.load()
+ image2.load()
+ return image1._new(image1.im.chop_add(image2.im, scale, offset))
+
+
+def subtract(image1, image2, scale=1.0, offset=0):
+ """
+ Subtracts two images, dividing the result by scale and adding the offset.
+ If omitted, scale defaults to 1.0, and offset to 0.0.
+
+ .. code-block:: python
+
+ out = ((image1 - image2) / scale + offset)
+
+ :rtype: :py:class:`~PIL.Image.Image`
+ """
+
+ image1.load()
+ image2.load()
+ return image1._new(image1.im.chop_subtract(image2.im, scale, offset))
+
+
+def add_modulo(image1, image2):
+ """Add two images, without clipping the result.
+
+ .. code-block:: python
+
+ out = ((image1 + image2) % MAX)
+
+ :rtype: :py:class:`~PIL.Image.Image`
+ """
+
+ image1.load()
+ image2.load()
+ return image1._new(image1.im.chop_add_modulo(image2.im))
+
+
+def subtract_modulo(image1, image2):
+ """Subtract two images, without clipping the result.
+
+ .. code-block:: python
+
+ out = ((image1 - image2) % MAX)
+
+ :rtype: :py:class:`~PIL.Image.Image`
+ """
+
+ image1.load()
+ image2.load()
+ return image1._new(image1.im.chop_subtract_modulo(image2.im))
+
+
+def logical_and(image1, image2):
+ """Logical AND between two images.
+
+ Both of the images must have mode "1". If you would like to perform a
+ logical AND on an image with a mode other than "1", try
+ :py:meth:`~PIL.ImageChops.multiply` instead, using a black-and-white mask
+ as the second image.
+
+ .. code-block:: python
+
+ out = ((image1 and image2) % MAX)
+
+ :rtype: :py:class:`~PIL.Image.Image`
+ """
+
+ image1.load()
+ image2.load()
+ return image1._new(image1.im.chop_and(image2.im))
+
+
+def logical_or(image1, image2):
+ """Logical OR between two images.
+
+ Both of the images must have mode "1".
+
+ .. code-block:: python
+
+ out = ((image1 or image2) % MAX)
+
+ :rtype: :py:class:`~PIL.Image.Image`
+ """
+
+ image1.load()
+ image2.load()
+ return image1._new(image1.im.chop_or(image2.im))
+
+
+def logical_xor(image1, image2):
+ """Logical XOR between two images.
+
+ Both of the images must have mode "1".
+
+ .. code-block:: python
+
+ out = ((bool(image1) != bool(image2)) % MAX)
+
+ :rtype: :py:class:`~PIL.Image.Image`
+ """
+
+ image1.load()
+ image2.load()
+ return image1._new(image1.im.chop_xor(image2.im))
+
+
+def blend(image1, image2, alpha):
+ """Blend images using constant transparency weight. Alias for
+ :py:func:`PIL.Image.blend`.
+
+ :rtype: :py:class:`~PIL.Image.Image`
+ """
+
+ return Image.blend(image1, image2, alpha)
+
+
+def composite(image1, image2, mask):
+ """Create composite using transparency mask. Alias for
+ :py:func:`PIL.Image.composite`.
+
+ :rtype: :py:class:`~PIL.Image.Image`
+ """
+
+ return Image.composite(image1, image2, mask)
+
+
+def offset(image, xoffset, yoffset=None):
+ """Returns a copy of the image where data has been offset by the given
+ distances. Data wraps around the edges. If ``yoffset`` is omitted, it
+ is assumed to be equal to ``xoffset``.
+
+ :param xoffset: The horizontal distance.
+ :param yoffset: The vertical distance. If omitted, both
+ distances are set to the same value.
+ :rtype: :py:class:`~PIL.Image.Image`
+ """
+
+ if yoffset is None:
+ yoffset = xoffset
+ image.load()
+ return image._new(image.im.offset(xoffset, yoffset))
diff --git a/venv/Lib/site-packages/PIL/ImageCms.py b/venv/Lib/site-packages/PIL/ImageCms.py
new file mode 100644
index 0000000..ea328e1
--- /dev/null
+++ b/venv/Lib/site-packages/PIL/ImageCms.py
@@ -0,0 +1,1029 @@
+# The Python Imaging Library.
+# $Id$
+
+# Optional color management support, based on Kevin Cazabon's PyCMS
+# library.
+
+# History:
+
+# 2009-03-08 fl Added to PIL.
+
+# Copyright (C) 2002-2003 Kevin Cazabon
+# Copyright (c) 2009 by Fredrik Lundh
+# Copyright (c) 2013 by Eric Soroos
+
+# See the README file for information on usage and redistribution. See
+# below for the original description.
+
+import sys
+import warnings
+from enum import IntEnum
+
+from PIL import Image
+
+try:
+ from PIL import _imagingcms
+except ImportError as ex:
+ # Allow error import for doc purposes, but error out when accessing
+ # anything in core.
+ from ._util import deferred_error
+
+ _imagingcms = deferred_error(ex)
+
+DESCRIPTION = """
+pyCMS
+
+ a Python / PIL interface to the littleCMS ICC Color Management System
+ Copyright (C) 2002-2003 Kevin Cazabon
+ kevin@cazabon.com
+ https://www.cazabon.com
+
+ pyCMS home page: https://www.cazabon.com/pyCMS
+ littleCMS home page: https://www.littlecms.com
+ (littleCMS is Copyright (C) 1998-2001 Marti Maria)
+
+ Originally released under LGPL. Graciously donated to PIL in
+ March 2009, for distribution under the standard PIL license
+
+ The pyCMS.py module provides a "clean" interface between Python/PIL and
+ pyCMSdll, taking care of some of the more complex handling of the direct
+ pyCMSdll functions, as well as error-checking and making sure that all
+ relevant data is kept together.
+
+ While it is possible to call pyCMSdll functions directly, it's not highly
+ recommended.
+
+ Version History:
+
+ 1.0.0 pil Oct 2013 Port to LCMS 2.
+
+ 0.1.0 pil mod March 10, 2009
+
+ Renamed display profile to proof profile. The proof
+ profile is the profile of the device that is being
+ simulated, not the profile of the device which is
+ actually used to display/print the final simulation
+ (that'd be the output profile) - also see LCMSAPI.txt
+ input colorspace -> using 'renderingIntent' -> proof
+ colorspace -> using 'proofRenderingIntent' -> output
+ colorspace
+
+ Added LCMS FLAGS support.
+ Added FLAGS["SOFTPROOFING"] as default flag for
+ buildProofTransform (otherwise the proof profile/intent
+ would be ignored).
+
+ 0.1.0 pil March 2009 - added to PIL, as PIL.ImageCms
+
+ 0.0.2 alpha Jan 6, 2002
+
+ Added try/except statements around type() checks of
+ potential CObjects... Python won't let you use type()
+ on them, and raises a TypeError (stupid, if you ask
+ me!)
+
+ Added buildProofTransformFromOpenProfiles() function.
+ Additional fixes in DLL, see DLL code for details.
+
+ 0.0.1 alpha first public release, Dec. 26, 2002
+
+ Known to-do list with current version (of Python interface, not pyCMSdll):
+
+ none
+
+"""
+
+VERSION = "1.0.0 pil"
+
+# --------------------------------------------------------------------.
+
+core = _imagingcms
+
+#
+# intent/direction values
+
+
+class Intent(IntEnum):
+ PERCEPTUAL = 0
+ RELATIVE_COLORIMETRIC = 1
+ SATURATION = 2
+ ABSOLUTE_COLORIMETRIC = 3
+
+
+class Direction(IntEnum):
+ INPUT = 0
+ OUTPUT = 1
+ PROOF = 2
+
+
+def __getattr__(name):
+ deprecated = "deprecated and will be removed in Pillow 10 (2023-07-01). "
+ for enum, prefix in {Intent: "INTENT_", Direction: "DIRECTION_"}.items():
+ if name.startswith(prefix):
+ name = name[len(prefix) :]
+ if name in enum.__members__:
+ warnings.warn(
+ prefix
+ + name
+ + " is "
+ + deprecated
+ + "Use "
+ + enum.__name__
+ + "."
+ + name
+ + " instead.",
+ DeprecationWarning,
+ stacklevel=2,
+ )
+ return enum[name]
+ raise AttributeError(f"module '{__name__}' has no attribute '{name}'")
+
+
+#
+# flags
+
+FLAGS = {
+ "MATRIXINPUT": 1,
+ "MATRIXOUTPUT": 2,
+ "MATRIXONLY": (1 | 2),
+ "NOWHITEONWHITEFIXUP": 4, # Don't hot fix scum dot
+ # Don't create prelinearization tables on precalculated transforms
+ # (internal use):
+ "NOPRELINEARIZATION": 16,
+ "GUESSDEVICECLASS": 32, # Guess device class (for transform2devicelink)
+ "NOTCACHE": 64, # Inhibit 1-pixel cache
+ "NOTPRECALC": 256,
+ "NULLTRANSFORM": 512, # Don't transform anyway
+ "HIGHRESPRECALC": 1024, # Use more memory to give better accuracy
+ "LOWRESPRECALC": 2048, # Use less memory to minimize resources
+ "WHITEBLACKCOMPENSATION": 8192,
+ "BLACKPOINTCOMPENSATION": 8192,
+ "GAMUTCHECK": 4096, # Out of Gamut alarm
+ "SOFTPROOFING": 16384, # Do softproofing
+ "PRESERVEBLACK": 32768, # Black preservation
+ "NODEFAULTRESOURCEDEF": 16777216, # CRD special
+ "GRIDPOINTS": lambda n: ((n) & 0xFF) << 16, # Gridpoints
+}
+
+_MAX_FLAG = 0
+for flag in FLAGS.values():
+ if isinstance(flag, int):
+ _MAX_FLAG = _MAX_FLAG | flag
+
+
+# --------------------------------------------------------------------.
+# Experimental PIL-level API
+# --------------------------------------------------------------------.
+
+##
+# Profile.
+
+
+class ImageCmsProfile:
+ def __init__(self, profile):
+ """
+ :param profile: Either a string representing a filename,
+ a file like object containing a profile or a
+ low-level profile object
+
+ """
+
+ if isinstance(profile, str):
+ if sys.platform == "win32":
+ profile_bytes_path = profile.encode()
+ try:
+ profile_bytes_path.decode("ascii")
+ except UnicodeDecodeError:
+ with open(profile, "rb") as f:
+ self._set(core.profile_frombytes(f.read()))
+ return
+ self._set(core.profile_open(profile), profile)
+ elif hasattr(profile, "read"):
+ self._set(core.profile_frombytes(profile.read()))
+ elif isinstance(profile, _imagingcms.CmsProfile):
+ self._set(profile)
+ else:
+ raise TypeError("Invalid type for Profile")
+
+ def _set(self, profile, filename=None):
+ self.profile = profile
+ self.filename = filename
+ if profile:
+ self.product_name = None # profile.product_name
+ self.product_info = None # profile.product_info
+ else:
+ self.product_name = None
+ self.product_info = None
+
+ def tobytes(self):
+ """
+ Returns the profile in a format suitable for embedding in
+ saved images.
+
+ :returns: a bytes object containing the ICC profile.
+ """
+
+ return core.profile_tobytes(self.profile)
+
+
+class ImageCmsTransform(Image.ImagePointHandler):
+
+ """
+ Transform. This can be used with the procedural API, or with the standard
+ :py:func:`~PIL.Image.Image.point` method.
+
+ Will return the output profile in the ``output.info['icc_profile']``.
+ """
+
+ def __init__(
+ self,
+ input,
+ output,
+ input_mode,
+ output_mode,
+ intent=Intent.PERCEPTUAL,
+ proof=None,
+ proof_intent=Intent.ABSOLUTE_COLORIMETRIC,
+ flags=0,
+ ):
+ if proof is None:
+ self.transform = core.buildTransform(
+ input.profile, output.profile, input_mode, output_mode, intent, flags
+ )
+ else:
+ self.transform = core.buildProofTransform(
+ input.profile,
+ output.profile,
+ proof.profile,
+ input_mode,
+ output_mode,
+ intent,
+ proof_intent,
+ flags,
+ )
+ # Note: inputMode and outputMode are for pyCMS compatibility only
+ self.input_mode = self.inputMode = input_mode
+ self.output_mode = self.outputMode = output_mode
+
+ self.output_profile = output
+
+ def point(self, im):
+ return self.apply(im)
+
+ def apply(self, im, imOut=None):
+ im.load()
+ if imOut is None:
+ imOut = Image.new(self.output_mode, im.size, None)
+ self.transform.apply(im.im.id, imOut.im.id)
+ imOut.info["icc_profile"] = self.output_profile.tobytes()
+ return imOut
+
+ def apply_in_place(self, im):
+ im.load()
+ if im.mode != self.output_mode:
+ raise ValueError("mode mismatch") # wrong output mode
+ self.transform.apply(im.im.id, im.im.id)
+ im.info["icc_profile"] = self.output_profile.tobytes()
+ return im
+
+
+def get_display_profile(handle=None):
+ """
+ (experimental) Fetches the profile for the current display device.
+
+ :returns: ``None`` if the profile is not known.
+ """
+
+ if sys.platform != "win32":
+ return None
+
+ from PIL import ImageWin
+
+ if isinstance(handle, ImageWin.HDC):
+ profile = core.get_display_profile_win32(handle, 1)
+ else:
+ profile = core.get_display_profile_win32(handle or 0)
+ if profile is None:
+ return None
+ return ImageCmsProfile(profile)
+
+
+# --------------------------------------------------------------------.
+# pyCMS compatible layer
+# --------------------------------------------------------------------.
+
+
+class PyCMSError(Exception):
+
+ """(pyCMS) Exception class.
+ This is used for all errors in the pyCMS API."""
+
+ pass
+
+
+def profileToProfile(
+ im,
+ inputProfile,
+ outputProfile,
+ renderingIntent=Intent.PERCEPTUAL,
+ outputMode=None,
+ inPlace=False,
+ flags=0,
+):
+ """
+ (pyCMS) Applies an ICC transformation to a given image, mapping from
+ ``inputProfile`` to ``outputProfile``.
+
+ If the input or output profiles specified are not valid filenames, a
+ :exc:`PyCMSError` will be raised. If ``inPlace`` is ``True`` and
+ ``outputMode != im.mode``, a :exc:`PyCMSError` will be raised.
+ If an error occurs during application of the profiles,
+ a :exc:`PyCMSError` will be raised.
+ If ``outputMode`` is not a mode supported by the ``outputProfile`` (or by pyCMS),
+ a :exc:`PyCMSError` will be raised.
+
+ This function applies an ICC transformation to im from ``inputProfile``'s
+ color space to ``outputProfile``'s color space using the specified rendering
+ intent to decide how to handle out-of-gamut colors.
+
+ ``outputMode`` can be used to specify that a color mode conversion is to
+ be done using these profiles, but the specified profiles must be able
+ to handle that mode. I.e., if converting im from RGB to CMYK using
+ profiles, the input profile must handle RGB data, and the output
+ profile must handle CMYK data.
+
+ :param im: An open :py:class:`~PIL.Image.Image` object (i.e. Image.new(...)
+ or Image.open(...), etc.)
+ :param inputProfile: String, as a valid filename path to the ICC input
+ profile you wish to use for this image, or a profile object
+ :param outputProfile: String, as a valid filename path to the ICC output
+ profile you wish to use for this image, or a profile object
+ :param renderingIntent: Integer (0-3) specifying the rendering intent you
+ wish to use for the transform
+
+ ImageCms.Intent.PERCEPTUAL = 0 (DEFAULT)
+ ImageCms.Intent.RELATIVE_COLORIMETRIC = 1
+ ImageCms.Intent.SATURATION = 2
+ ImageCms.Intent.ABSOLUTE_COLORIMETRIC = 3
+
+ see the pyCMS documentation for details on rendering intents and what
+ they do.
+ :param outputMode: A valid PIL mode for the output image (i.e. "RGB",
+ "CMYK", etc.). Note: if rendering the image "inPlace", outputMode
+ MUST be the same mode as the input, or omitted completely. If
+ omitted, the outputMode will be the same as the mode of the input
+ image (im.mode)
+ :param inPlace: Boolean. If ``True``, the original image is modified in-place,
+ and ``None`` is returned. If ``False`` (default), a new
+ :py:class:`~PIL.Image.Image` object is returned with the transform applied.
+ :param flags: Integer (0-...) specifying additional flags
+ :returns: Either None or a new :py:class:`~PIL.Image.Image` object, depending on
+ the value of ``inPlace``
+ :exception PyCMSError:
+ """
+
+ if outputMode is None:
+ outputMode = im.mode
+
+ if not isinstance(renderingIntent, int) or not (0 <= renderingIntent <= 3):
+ raise PyCMSError("renderingIntent must be an integer between 0 and 3")
+
+ if not isinstance(flags, int) or not (0 <= flags <= _MAX_FLAG):
+ raise PyCMSError("flags must be an integer between 0 and %s" + _MAX_FLAG)
+
+ try:
+ if not isinstance(inputProfile, ImageCmsProfile):
+ inputProfile = ImageCmsProfile(inputProfile)
+ if not isinstance(outputProfile, ImageCmsProfile):
+ outputProfile = ImageCmsProfile(outputProfile)
+ transform = ImageCmsTransform(
+ inputProfile,
+ outputProfile,
+ im.mode,
+ outputMode,
+ renderingIntent,
+ flags=flags,
+ )
+ if inPlace:
+ transform.apply_in_place(im)
+ imOut = None
+ else:
+ imOut = transform.apply(im)
+ except (OSError, TypeError, ValueError) as v:
+ raise PyCMSError(v) from v
+
+ return imOut
+
+
+def getOpenProfile(profileFilename):
+ """
+ (pyCMS) Opens an ICC profile file.
+
+ The PyCMSProfile object can be passed back into pyCMS for use in creating
+ transforms and such (as in ImageCms.buildTransformFromOpenProfiles()).
+
+ If ``profileFilename`` is not a valid filename for an ICC profile,
+ a :exc:`PyCMSError` will be raised.
+
+ :param profileFilename: String, as a valid filename path to the ICC profile
+ you wish to open, or a file-like object.
+ :returns: A CmsProfile class object.
+ :exception PyCMSError:
+ """
+
+ try:
+ return ImageCmsProfile(profileFilename)
+ except (OSError, TypeError, ValueError) as v:
+ raise PyCMSError(v) from v
+
+
+def buildTransform(
+ inputProfile,
+ outputProfile,
+ inMode,
+ outMode,
+ renderingIntent=Intent.PERCEPTUAL,
+ flags=0,
+):
+ """
+ (pyCMS) Builds an ICC transform mapping from the ``inputProfile`` to the
+ ``outputProfile``. Use applyTransform to apply the transform to a given
+ image.
+
+ If the input or output profiles specified are not valid filenames, a
+ :exc:`PyCMSError` will be raised. If an error occurs during creation
+ of the transform, a :exc:`PyCMSError` will be raised.
+
+ If ``inMode`` or ``outMode`` are not a mode supported by the ``outputProfile``
+ (or by pyCMS), a :exc:`PyCMSError` will be raised.
+
+ This function builds and returns an ICC transform from the ``inputProfile``
+ to the ``outputProfile`` using the ``renderingIntent`` to determine what to do
+ with out-of-gamut colors. It will ONLY work for converting images that
+ are in ``inMode`` to images that are in ``outMode`` color format (PIL mode,
+ i.e. "RGB", "RGBA", "CMYK", etc.).
+
+ Building the transform is a fair part of the overhead in
+ ImageCms.profileToProfile(), so if you're planning on converting multiple
+ images using the same input/output settings, this can save you time.
+ Once you have a transform object, it can be used with
+ ImageCms.applyProfile() to convert images without the need to re-compute
+ the lookup table for the transform.
+
+ The reason pyCMS returns a class object rather than a handle directly
+ to the transform is that it needs to keep track of the PIL input/output
+ modes that the transform is meant for. These attributes are stored in
+ the ``inMode`` and ``outMode`` attributes of the object (which can be
+ manually overridden if you really want to, but I don't know of any
+ time that would be of use, or would even work).
+
+ :param inputProfile: String, as a valid filename path to the ICC input
+ profile you wish to use for this transform, or a profile object
+ :param outputProfile: String, as a valid filename path to the ICC output
+ profile you wish to use for this transform, or a profile object
+ :param inMode: String, as a valid PIL mode that the appropriate profile
+ also supports (i.e. "RGB", "RGBA", "CMYK", etc.)
+ :param outMode: String, as a valid PIL mode that the appropriate profile
+ also supports (i.e. "RGB", "RGBA", "CMYK", etc.)
+ :param renderingIntent: Integer (0-3) specifying the rendering intent you
+ wish to use for the transform
+
+ ImageCms.Intent.PERCEPTUAL = 0 (DEFAULT)
+ ImageCms.Intent.RELATIVE_COLORIMETRIC = 1
+ ImageCms.Intent.SATURATION = 2
+ ImageCms.Intent.ABSOLUTE_COLORIMETRIC = 3
+
+ see the pyCMS documentation for details on rendering intents and what
+ they do.
+ :param flags: Integer (0-...) specifying additional flags
+ :returns: A CmsTransform class object.
+ :exception PyCMSError:
+ """
+
+ if not isinstance(renderingIntent, int) or not (0 <= renderingIntent <= 3):
+ raise PyCMSError("renderingIntent must be an integer between 0 and 3")
+
+ if not isinstance(flags, int) or not (0 <= flags <= _MAX_FLAG):
+ raise PyCMSError("flags must be an integer between 0 and %s" + _MAX_FLAG)
+
+ try:
+ if not isinstance(inputProfile, ImageCmsProfile):
+ inputProfile = ImageCmsProfile(inputProfile)
+ if not isinstance(outputProfile, ImageCmsProfile):
+ outputProfile = ImageCmsProfile(outputProfile)
+ return ImageCmsTransform(
+ inputProfile, outputProfile, inMode, outMode, renderingIntent, flags=flags
+ )
+ except (OSError, TypeError, ValueError) as v:
+ raise PyCMSError(v) from v
+
+
+def buildProofTransform(
+ inputProfile,
+ outputProfile,
+ proofProfile,
+ inMode,
+ outMode,
+ renderingIntent=Intent.PERCEPTUAL,
+ proofRenderingIntent=Intent.ABSOLUTE_COLORIMETRIC,
+ flags=FLAGS["SOFTPROOFING"],
+):
+ """
+ (pyCMS) Builds an ICC transform mapping from the ``inputProfile`` to the
+ ``outputProfile``, but tries to simulate the result that would be
+ obtained on the ``proofProfile`` device.
+
+ If the input, output, or proof profiles specified are not valid
+ filenames, a :exc:`PyCMSError` will be raised.
+
+ If an error occurs during creation of the transform,
+ a :exc:`PyCMSError` will be raised.
+
+ If ``inMode`` or ``outMode`` are not a mode supported by the ``outputProfile``
+ (or by pyCMS), a :exc:`PyCMSError` will be raised.
+
+ This function builds and returns an ICC transform from the ``inputProfile``
+ to the ``outputProfile``, but tries to simulate the result that would be
+ obtained on the ``proofProfile`` device using ``renderingIntent`` and
+ ``proofRenderingIntent`` to determine what to do with out-of-gamut
+ colors. This is known as "soft-proofing". It will ONLY work for
+ converting images that are in ``inMode`` to images that are in outMode
+ color format (PIL mode, i.e. "RGB", "RGBA", "CMYK", etc.).
+
+ Usage of the resulting transform object is exactly the same as with
+ ImageCms.buildTransform().
+
+ Proof profiling is generally used when using an output device to get a
+ good idea of what the final printed/displayed image would look like on
+ the ``proofProfile`` device when it's quicker and easier to use the
+ output device for judging color. Generally, this means that the
+ output device is a monitor, or a dye-sub printer (etc.), and the simulated
+ device is something more expensive, complicated, or time consuming
+ (making it difficult to make a real print for color judgement purposes).
+
+ Soft-proofing basically functions by adjusting the colors on the
+ output device to match the colors of the device being simulated. However,
+ when the simulated device has a much wider gamut than the output
+ device, you may obtain marginal results.
+
+ :param inputProfile: String, as a valid filename path to the ICC input
+ profile you wish to use for this transform, or a profile object
+ :param outputProfile: String, as a valid filename path to the ICC output
+ (monitor, usually) profile you wish to use for this transform, or a
+ profile object
+ :param proofProfile: String, as a valid filename path to the ICC proof
+ profile you wish to use for this transform, or a profile object
+ :param inMode: String, as a valid PIL mode that the appropriate profile
+ also supports (i.e. "RGB", "RGBA", "CMYK", etc.)
+ :param outMode: String, as a valid PIL mode that the appropriate profile
+ also supports (i.e. "RGB", "RGBA", "CMYK", etc.)
+ :param renderingIntent: Integer (0-3) specifying the rendering intent you
+ wish to use for the input->proof (simulated) transform
+
+ ImageCms.Intent.PERCEPTUAL = 0 (DEFAULT)
+ ImageCms.Intent.RELATIVE_COLORIMETRIC = 1
+ ImageCms.Intent.SATURATION = 2
+ ImageCms.Intent.ABSOLUTE_COLORIMETRIC = 3
+
+ see the pyCMS documentation for details on rendering intents and what
+ they do.
+ :param proofRenderingIntent: Integer (0-3) specifying the rendering intent
+ you wish to use for proof->output transform
+
+ ImageCms.Intent.PERCEPTUAL = 0 (DEFAULT)
+ ImageCms.Intent.RELATIVE_COLORIMETRIC = 1
+ ImageCms.Intent.SATURATION = 2
+ ImageCms.Intent.ABSOLUTE_COLORIMETRIC = 3
+
+ see the pyCMS documentation for details on rendering intents and what
+ they do.
+ :param flags: Integer (0-...) specifying additional flags
+ :returns: A CmsTransform class object.
+ :exception PyCMSError:
+ """
+
+ if not isinstance(renderingIntent, int) or not (0 <= renderingIntent <= 3):
+ raise PyCMSError("renderingIntent must be an integer between 0 and 3")
+
+ if not isinstance(flags, int) or not (0 <= flags <= _MAX_FLAG):
+ raise PyCMSError("flags must be an integer between 0 and %s" + _MAX_FLAG)
+
+ try:
+ if not isinstance(inputProfile, ImageCmsProfile):
+ inputProfile = ImageCmsProfile(inputProfile)
+ if not isinstance(outputProfile, ImageCmsProfile):
+ outputProfile = ImageCmsProfile(outputProfile)
+ if not isinstance(proofProfile, ImageCmsProfile):
+ proofProfile = ImageCmsProfile(proofProfile)
+ return ImageCmsTransform(
+ inputProfile,
+ outputProfile,
+ inMode,
+ outMode,
+ renderingIntent,
+ proofProfile,
+ proofRenderingIntent,
+ flags,
+ )
+ except (OSError, TypeError, ValueError) as v:
+ raise PyCMSError(v) from v
+
+
+buildTransformFromOpenProfiles = buildTransform
+buildProofTransformFromOpenProfiles = buildProofTransform
+
+
+def applyTransform(im, transform, inPlace=False):
+ """
+ (pyCMS) Applies a transform to a given image.
+
+ If ``im.mode != transform.inMode``, a :exc:`PyCMSError` is raised.
+
+ If ``inPlace`` is ``True`` and ``transform.inMode != transform.outMode``, a
+ :exc:`PyCMSError` is raised.
+
+ If ``im.mode``, ``transform.inMode`` or ``transform.outMode`` is not
+ supported by pyCMSdll or the profiles you used for the transform, a
+ :exc:`PyCMSError` is raised.
+
+ If an error occurs while the transform is being applied,
+ a :exc:`PyCMSError` is raised.
+
+ This function applies a pre-calculated transform (from
+ ImageCms.buildTransform() or ImageCms.buildTransformFromOpenProfiles())
+ to an image. The transform can be used for multiple images, saving
+ considerable calculation time if doing the same conversion multiple times.
+
+ If you want to modify im in-place instead of receiving a new image as
+ the return value, set ``inPlace`` to ``True``. This can only be done if
+ ``transform.inMode`` and ``transform.outMode`` are the same, because we can't
+ change the mode in-place (the buffer sizes for some modes are
+ different). The default behavior is to return a new :py:class:`~PIL.Image.Image`
+ object of the same dimensions in mode ``transform.outMode``.
+
+ :param im: An :py:class:`~PIL.Image.Image` object, and im.mode must be the same
+ as the ``inMode`` supported by the transform.
+ :param transform: A valid CmsTransform class object
+ :param inPlace: Bool. If ``True``, ``im`` is modified in place and ``None`` is
+ returned, if ``False``, a new :py:class:`~PIL.Image.Image` object with the
+ transform applied is returned (and ``im`` is not changed). The default is
+ ``False``.
+ :returns: Either ``None``, or a new :py:class:`~PIL.Image.Image` object,
+ depending on the value of ``inPlace``. The profile will be returned in
+ the image's ``info['icc_profile']``.
+ :exception PyCMSError:
+ """
+
+ try:
+ if inPlace:
+ transform.apply_in_place(im)
+ imOut = None
+ else:
+ imOut = transform.apply(im)
+ except (TypeError, ValueError) as v:
+ raise PyCMSError(v) from v
+
+ return imOut
+
+
+def createProfile(colorSpace, colorTemp=-1):
+ """
+ (pyCMS) Creates a profile.
+
+ If colorSpace not in ``["LAB", "XYZ", "sRGB"]``,
+ a :exc:`PyCMSError` is raised.
+
+ If using LAB and ``colorTemp`` is not a positive integer,
+ a :exc:`PyCMSError` is raised.
+
+ If an error occurs while creating the profile,
+ a :exc:`PyCMSError` is raised.
+
+ Use this function to create common profiles on-the-fly instead of
+ having to supply a profile on disk and knowing the path to it. It
+ returns a normal CmsProfile object that can be passed to
+ ImageCms.buildTransformFromOpenProfiles() to create a transform to apply
+ to images.
+
+ :param colorSpace: String, the color space of the profile you wish to
+ create.
+ Currently only "LAB", "XYZ", and "sRGB" are supported.
+ :param colorTemp: Positive integer for the white point for the profile, in
+ degrees Kelvin (i.e. 5000, 6500, 9600, etc.). The default is for D50
+ illuminant if omitted (5000k). colorTemp is ONLY applied to LAB
+ profiles, and is ignored for XYZ and sRGB.
+ :returns: A CmsProfile class object
+ :exception PyCMSError:
+ """
+
+ if colorSpace not in ["LAB", "XYZ", "sRGB"]:
+ raise PyCMSError(
+ f"Color space not supported for on-the-fly profile creation ({colorSpace})"
+ )
+
+ if colorSpace == "LAB":
+ try:
+ colorTemp = float(colorTemp)
+ except (TypeError, ValueError) as e:
+ raise PyCMSError(
+ f'Color temperature must be numeric, "{colorTemp}" not valid'
+ ) from e
+
+ try:
+ return core.createProfile(colorSpace, colorTemp)
+ except (TypeError, ValueError) as v:
+ raise PyCMSError(v) from v
+
+
+def getProfileName(profile):
+ """
+
+ (pyCMS) Gets the internal product name for the given profile.
+
+ If ``profile`` isn't a valid CmsProfile object or filename to a profile,
+ a :exc:`PyCMSError` is raised If an error occurs while trying
+ to obtain the name tag, a :exc:`PyCMSError` is raised.
+
+ Use this function to obtain the INTERNAL name of the profile (stored
+ in an ICC tag in the profile itself), usually the one used when the
+ profile was originally created. Sometimes this tag also contains
+ additional information supplied by the creator.
+
+ :param profile: EITHER a valid CmsProfile object, OR a string of the
+ filename of an ICC profile.
+ :returns: A string containing the internal name of the profile as stored
+ in an ICC tag.
+ :exception PyCMSError:
+ """
+
+ try:
+ # add an extra newline to preserve pyCMS compatibility
+ if not isinstance(profile, ImageCmsProfile):
+ profile = ImageCmsProfile(profile)
+ # do it in python, not c.
+ # // name was "%s - %s" (model, manufacturer) || Description ,
+ # // but if the Model and Manufacturer were the same or the model
+ # // was long, Just the model, in 1.x
+ model = profile.profile.model
+ manufacturer = profile.profile.manufacturer
+
+ if not (model or manufacturer):
+ return (profile.profile.profile_description or "") + "\n"
+ if not manufacturer or len(model) > 30:
+ return model + "\n"
+ return f"{model} - {manufacturer}\n"
+
+ except (AttributeError, OSError, TypeError, ValueError) as v:
+ raise PyCMSError(v) from v
+
+
+def getProfileInfo(profile):
+ """
+ (pyCMS) Gets the internal product information for the given profile.
+
+ If ``profile`` isn't a valid CmsProfile object or filename to a profile,
+ a :exc:`PyCMSError` is raised.
+
+ If an error occurs while trying to obtain the info tag,
+ a :exc:`PyCMSError` is raised.
+
+ Use this function to obtain the information stored in the profile's
+ info tag. This often contains details about the profile, and how it
+ was created, as supplied by the creator.
+
+ :param profile: EITHER a valid CmsProfile object, OR a string of the
+ filename of an ICC profile.
+ :returns: A string containing the internal profile information stored in
+ an ICC tag.
+ :exception PyCMSError:
+ """
+
+ try:
+ if not isinstance(profile, ImageCmsProfile):
+ profile = ImageCmsProfile(profile)
+ # add an extra newline to preserve pyCMS compatibility
+ # Python, not C. the white point bits weren't working well,
+ # so skipping.
+ # info was description \r\n\r\n copyright \r\n\r\n K007 tag \r\n\r\n whitepoint
+ description = profile.profile.profile_description
+ cpright = profile.profile.copyright
+ arr = []
+ for elt in (description, cpright):
+ if elt:
+ arr.append(elt)
+ return "\r\n\r\n".join(arr) + "\r\n\r\n"
+
+ except (AttributeError, OSError, TypeError, ValueError) as v:
+ raise PyCMSError(v) from v
+
+
+def getProfileCopyright(profile):
+ """
+ (pyCMS) Gets the copyright for the given profile.
+
+ If ``profile`` isn't a valid CmsProfile object or filename to a profile, a
+ :exc:`PyCMSError` is raised.
+
+ If an error occurs while trying to obtain the copyright tag,
+ a :exc:`PyCMSError` is raised.
+
+ Use this function to obtain the information stored in the profile's
+ copyright tag.
+
+ :param profile: EITHER a valid CmsProfile object, OR a string of the
+ filename of an ICC profile.
+ :returns: A string containing the internal profile information stored in
+ an ICC tag.
+ :exception PyCMSError:
+ """
+ try:
+ # add an extra newline to preserve pyCMS compatibility
+ if not isinstance(profile, ImageCmsProfile):
+ profile = ImageCmsProfile(profile)
+ return (profile.profile.copyright or "") + "\n"
+ except (AttributeError, OSError, TypeError, ValueError) as v:
+ raise PyCMSError(v) from v
+
+
+def getProfileManufacturer(profile):
+ """
+ (pyCMS) Gets the manufacturer for the given profile.
+
+ If ``profile`` isn't a valid CmsProfile object or filename to a profile, a
+ :exc:`PyCMSError` is raised.
+
+ If an error occurs while trying to obtain the manufacturer tag, a
+ :exc:`PyCMSError` is raised.
+
+ Use this function to obtain the information stored in the profile's
+ manufacturer tag.
+
+ :param profile: EITHER a valid CmsProfile object, OR a string of the
+ filename of an ICC profile.
+ :returns: A string containing the internal profile information stored in
+ an ICC tag.
+ :exception PyCMSError:
+ """
+ try:
+ # add an extra newline to preserve pyCMS compatibility
+ if not isinstance(profile, ImageCmsProfile):
+ profile = ImageCmsProfile(profile)
+ return (profile.profile.manufacturer or "") + "\n"
+ except (AttributeError, OSError, TypeError, ValueError) as v:
+ raise PyCMSError(v) from v
+
+
+def getProfileModel(profile):
+ """
+ (pyCMS) Gets the model for the given profile.
+
+ If ``profile`` isn't a valid CmsProfile object or filename to a profile, a
+ :exc:`PyCMSError` is raised.
+
+ If an error occurs while trying to obtain the model tag,
+ a :exc:`PyCMSError` is raised.
+
+ Use this function to obtain the information stored in the profile's
+ model tag.
+
+ :param profile: EITHER a valid CmsProfile object, OR a string of the
+ filename of an ICC profile.
+ :returns: A string containing the internal profile information stored in
+ an ICC tag.
+ :exception PyCMSError:
+ """
+
+ try:
+ # add an extra newline to preserve pyCMS compatibility
+ if not isinstance(profile, ImageCmsProfile):
+ profile = ImageCmsProfile(profile)
+ return (profile.profile.model or "") + "\n"
+ except (AttributeError, OSError, TypeError, ValueError) as v:
+ raise PyCMSError(v) from v
+
+
+def getProfileDescription(profile):
+ """
+ (pyCMS) Gets the description for the given profile.
+
+ If ``profile`` isn't a valid CmsProfile object or filename to a profile, a
+ :exc:`PyCMSError` is raised.
+
+ If an error occurs while trying to obtain the description tag,
+ a :exc:`PyCMSError` is raised.
+
+ Use this function to obtain the information stored in the profile's
+ description tag.
+
+ :param profile: EITHER a valid CmsProfile object, OR a string of the
+ filename of an ICC profile.
+ :returns: A string containing the internal profile information stored in an
+ ICC tag.
+ :exception PyCMSError:
+ """
+
+ try:
+ # add an extra newline to preserve pyCMS compatibility
+ if not isinstance(profile, ImageCmsProfile):
+ profile = ImageCmsProfile(profile)
+ return (profile.profile.profile_description or "") + "\n"
+ except (AttributeError, OSError, TypeError, ValueError) as v:
+ raise PyCMSError(v) from v
+
+
+def getDefaultIntent(profile):
+ """
+ (pyCMS) Gets the default intent name for the given profile.
+
+ If ``profile`` isn't a valid CmsProfile object or filename to a profile, a
+ :exc:`PyCMSError` is raised.
+
+ If an error occurs while trying to obtain the default intent, a
+ :exc:`PyCMSError` is raised.
+
+ Use this function to determine the default (and usually best optimized)
+ rendering intent for this profile. Most profiles support multiple
+ rendering intents, but are intended mostly for one type of conversion.
+ If you wish to use a different intent than returned, use
+ ImageCms.isIntentSupported() to verify it will work first.
+
+ :param profile: EITHER a valid CmsProfile object, OR a string of the
+ filename of an ICC profile.
+ :returns: Integer 0-3 specifying the default rendering intent for this
+ profile.
+
+ ImageCms.Intent.PERCEPTUAL = 0 (DEFAULT)
+ ImageCms.Intent.RELATIVE_COLORIMETRIC = 1
+ ImageCms.Intent.SATURATION = 2
+ ImageCms.Intent.ABSOLUTE_COLORIMETRIC = 3
+
+ see the pyCMS documentation for details on rendering intents and what
+ they do.
+ :exception PyCMSError:
+ """
+
+ try:
+ if not isinstance(profile, ImageCmsProfile):
+ profile = ImageCmsProfile(profile)
+ return profile.profile.rendering_intent
+ except (AttributeError, OSError, TypeError, ValueError) as v:
+ raise PyCMSError(v) from v
+
+
+def isIntentSupported(profile, intent, direction):
+ """
+ (pyCMS) Checks if a given intent is supported.
+
+ Use this function to verify that you can use your desired
+ ``intent`` with ``profile``, and that ``profile`` can be used for the
+ input/output/proof profile as you desire.
+
+ Some profiles are created specifically for one "direction", can cannot
+ be used for others. Some profiles can only be used for certain
+ rendering intents, so it's best to either verify this before trying
+ to create a transform with them (using this function), or catch the
+ potential :exc:`PyCMSError` that will occur if they don't
+ support the modes you select.
+
+ :param profile: EITHER a valid CmsProfile object, OR a string of the
+ filename of an ICC profile.
+ :param intent: Integer (0-3) specifying the rendering intent you wish to
+ use with this profile
+
+ ImageCms.Intent.PERCEPTUAL = 0 (DEFAULT)
+ ImageCms.Intent.RELATIVE_COLORIMETRIC = 1
+ ImageCms.Intent.SATURATION = 2
+ ImageCms.Intent.ABSOLUTE_COLORIMETRIC = 3
+
+ see the pyCMS documentation for details on rendering intents and what
+ they do.
+ :param direction: Integer specifying if the profile is to be used for
+ input, output, or proof
+
+ INPUT = 0 (or use ImageCms.Direction.INPUT)
+ OUTPUT = 1 (or use ImageCms.Direction.OUTPUT)
+ PROOF = 2 (or use ImageCms.Direction.PROOF)
+
+ :returns: 1 if the intent/direction are supported, -1 if they are not.
+ :exception PyCMSError:
+ """
+
+ try:
+ if not isinstance(profile, ImageCmsProfile):
+ profile = ImageCmsProfile(profile)
+ # FIXME: I get different results for the same data w. different
+ # compilers. Bug in LittleCMS or in the binding?
+ if profile.profile.is_intent_supported(intent, direction):
+ return 1
+ else:
+ return -1
+ except (AttributeError, OSError, TypeError, ValueError) as v:
+ raise PyCMSError(v) from v
+
+
+def versions():
+ """
+ (pyCMS) Fetches versions.
+ """
+
+ return (VERSION, core.littlecms_version, sys.version.split()[0], Image.__version__)
diff --git a/venv/Lib/site-packages/PIL/ImageColor.py b/venv/Lib/site-packages/PIL/ImageColor.py
new file mode 100644
index 0000000..25f92f2
--- /dev/null
+++ b/venv/Lib/site-packages/PIL/ImageColor.py
@@ -0,0 +1,302 @@
+#
+# The Python Imaging Library
+# $Id$
+#
+# map CSS3-style colour description strings to RGB
+#
+# History:
+# 2002-10-24 fl Added support for CSS-style color strings
+# 2002-12-15 fl Added RGBA support
+# 2004-03-27 fl Fixed remaining int() problems for Python 1.5.2
+# 2004-07-19 fl Fixed gray/grey spelling issues
+# 2009-03-05 fl Fixed rounding error in grayscale calculation
+#
+# Copyright (c) 2002-2004 by Secret Labs AB
+# Copyright (c) 2002-2004 by Fredrik Lundh
+#
+# See the README file for information on usage and redistribution.
+#
+
+import re
+
+from . import Image
+
+
+def getrgb(color):
+ """
+ Convert a color string to an RGB or RGBA tuple. If the string cannot be
+ parsed, this function raises a :py:exc:`ValueError` exception.
+
+ .. versionadded:: 1.1.4
+
+ :param color: A color string
+ :return: ``(red, green, blue[, alpha])``
+ """
+ if len(color) > 100:
+ raise ValueError("color specifier is too long")
+ color = color.lower()
+
+ rgb = colormap.get(color, None)
+ if rgb:
+ if isinstance(rgb, tuple):
+ return rgb
+ colormap[color] = rgb = getrgb(rgb)
+ return rgb
+
+ # check for known string formats
+ if re.match("#[a-f0-9]{3}$", color):
+ return (int(color[1] * 2, 16), int(color[2] * 2, 16), int(color[3] * 2, 16))
+
+ if re.match("#[a-f0-9]{4}$", color):
+ return (
+ int(color[1] * 2, 16),
+ int(color[2] * 2, 16),
+ int(color[3] * 2, 16),
+ int(color[4] * 2, 16),
+ )
+
+ if re.match("#[a-f0-9]{6}$", color):
+ return (int(color[1:3], 16), int(color[3:5], 16), int(color[5:7], 16))
+
+ if re.match("#[a-f0-9]{8}$", color):
+ return (
+ int(color[1:3], 16),
+ int(color[3:5], 16),
+ int(color[5:7], 16),
+ int(color[7:9], 16),
+ )
+
+ m = re.match(r"rgb\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)$", color)
+ if m:
+ return (int(m.group(1)), int(m.group(2)), int(m.group(3)))
+
+ m = re.match(r"rgb\(\s*(\d+)%\s*,\s*(\d+)%\s*,\s*(\d+)%\s*\)$", color)
+ if m:
+ return (
+ int((int(m.group(1)) * 255) / 100.0 + 0.5),
+ int((int(m.group(2)) * 255) / 100.0 + 0.5),
+ int((int(m.group(3)) * 255) / 100.0 + 0.5),
+ )
+
+ m = re.match(
+ r"hsl\(\s*(\d+\.?\d*)\s*,\s*(\d+\.?\d*)%\s*,\s*(\d+\.?\d*)%\s*\)$", color
+ )
+ if m:
+ from colorsys import hls_to_rgb
+
+ rgb = hls_to_rgb(
+ float(m.group(1)) / 360.0,
+ float(m.group(3)) / 100.0,
+ float(m.group(2)) / 100.0,
+ )
+ return (
+ int(rgb[0] * 255 + 0.5),
+ int(rgb[1] * 255 + 0.5),
+ int(rgb[2] * 255 + 0.5),
+ )
+
+ m = re.match(
+ r"hs[bv]\(\s*(\d+\.?\d*)\s*,\s*(\d+\.?\d*)%\s*,\s*(\d+\.?\d*)%\s*\)$", color
+ )
+ if m:
+ from colorsys import hsv_to_rgb
+
+ rgb = hsv_to_rgb(
+ float(m.group(1)) / 360.0,
+ float(m.group(2)) / 100.0,
+ float(m.group(3)) / 100.0,
+ )
+ return (
+ int(rgb[0] * 255 + 0.5),
+ int(rgb[1] * 255 + 0.5),
+ int(rgb[2] * 255 + 0.5),
+ )
+
+ m = re.match(r"rgba\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)$", color)
+ if m:
+ return (int(m.group(1)), int(m.group(2)), int(m.group(3)), int(m.group(4)))
+ raise ValueError(f"unknown color specifier: {repr(color)}")
+
+
+def getcolor(color, mode):
+ """
+ Same as :py:func:`~PIL.ImageColor.getrgb`, but converts the RGB value to a
+ greyscale value if the mode is not color or a palette image. If the string
+ cannot be parsed, this function raises a :py:exc:`ValueError` exception.
+
+ .. versionadded:: 1.1.4
+
+ :param color: A color string
+ :return: ``(graylevel [, alpha]) or (red, green, blue[, alpha])``
+ """
+ # same as getrgb, but converts the result to the given mode
+ color, alpha = getrgb(color), 255
+ if len(color) == 4:
+ color, alpha = color[0:3], color[3]
+
+ if Image.getmodebase(mode) == "L":
+ r, g, b = color
+ # ITU-R Recommendation 601-2 for nonlinear RGB
+ # scaled to 24 bits to match the convert's implementation.
+ color = (r * 19595 + g * 38470 + b * 7471 + 0x8000) >> 16
+ if mode[-1] == "A":
+ return (color, alpha)
+ else:
+ if mode[-1] == "A":
+ return color + (alpha,)
+ return color
+
+
+colormap = {
+ # X11 colour table from https://drafts.csswg.org/css-color-4/, with
+ # gray/grey spelling issues fixed. This is a superset of HTML 4.0
+ # colour names used in CSS 1.
+ "aliceblue": "#f0f8ff",
+ "antiquewhite": "#faebd7",
+ "aqua": "#00ffff",
+ "aquamarine": "#7fffd4",
+ "azure": "#f0ffff",
+ "beige": "#f5f5dc",
+ "bisque": "#ffe4c4",
+ "black": "#000000",
+ "blanchedalmond": "#ffebcd",
+ "blue": "#0000ff",
+ "blueviolet": "#8a2be2",
+ "brown": "#a52a2a",
+ "burlywood": "#deb887",
+ "cadetblue": "#5f9ea0",
+ "chartreuse": "#7fff00",
+ "chocolate": "#d2691e",
+ "coral": "#ff7f50",
+ "cornflowerblue": "#6495ed",
+ "cornsilk": "#fff8dc",
+ "crimson": "#dc143c",
+ "cyan": "#00ffff",
+ "darkblue": "#00008b",
+ "darkcyan": "#008b8b",
+ "darkgoldenrod": "#b8860b",
+ "darkgray": "#a9a9a9",
+ "darkgrey": "#a9a9a9",
+ "darkgreen": "#006400",
+ "darkkhaki": "#bdb76b",
+ "darkmagenta": "#8b008b",
+ "darkolivegreen": "#556b2f",
+ "darkorange": "#ff8c00",
+ "darkorchid": "#9932cc",
+ "darkred": "#8b0000",
+ "darksalmon": "#e9967a",
+ "darkseagreen": "#8fbc8f",
+ "darkslateblue": "#483d8b",
+ "darkslategray": "#2f4f4f",
+ "darkslategrey": "#2f4f4f",
+ "darkturquoise": "#00ced1",
+ "darkviolet": "#9400d3",
+ "deeppink": "#ff1493",
+ "deepskyblue": "#00bfff",
+ "dimgray": "#696969",
+ "dimgrey": "#696969",
+ "dodgerblue": "#1e90ff",
+ "firebrick": "#b22222",
+ "floralwhite": "#fffaf0",
+ "forestgreen": "#228b22",
+ "fuchsia": "#ff00ff",
+ "gainsboro": "#dcdcdc",
+ "ghostwhite": "#f8f8ff",
+ "gold": "#ffd700",
+ "goldenrod": "#daa520",
+ "gray": "#808080",
+ "grey": "#808080",
+ "green": "#008000",
+ "greenyellow": "#adff2f",
+ "honeydew": "#f0fff0",
+ "hotpink": "#ff69b4",
+ "indianred": "#cd5c5c",
+ "indigo": "#4b0082",
+ "ivory": "#fffff0",
+ "khaki": "#f0e68c",
+ "lavender": "#e6e6fa",
+ "lavenderblush": "#fff0f5",
+ "lawngreen": "#7cfc00",
+ "lemonchiffon": "#fffacd",
+ "lightblue": "#add8e6",
+ "lightcoral": "#f08080",
+ "lightcyan": "#e0ffff",
+ "lightgoldenrodyellow": "#fafad2",
+ "lightgreen": "#90ee90",
+ "lightgray": "#d3d3d3",
+ "lightgrey": "#d3d3d3",
+ "lightpink": "#ffb6c1",
+ "lightsalmon": "#ffa07a",
+ "lightseagreen": "#20b2aa",
+ "lightskyblue": "#87cefa",
+ "lightslategray": "#778899",
+ "lightslategrey": "#778899",
+ "lightsteelblue": "#b0c4de",
+ "lightyellow": "#ffffe0",
+ "lime": "#00ff00",
+ "limegreen": "#32cd32",
+ "linen": "#faf0e6",
+ "magenta": "#ff00ff",
+ "maroon": "#800000",
+ "mediumaquamarine": "#66cdaa",
+ "mediumblue": "#0000cd",
+ "mediumorchid": "#ba55d3",
+ "mediumpurple": "#9370db",
+ "mediumseagreen": "#3cb371",
+ "mediumslateblue": "#7b68ee",
+ "mediumspringgreen": "#00fa9a",
+ "mediumturquoise": "#48d1cc",
+ "mediumvioletred": "#c71585",
+ "midnightblue": "#191970",
+ "mintcream": "#f5fffa",
+ "mistyrose": "#ffe4e1",
+ "moccasin": "#ffe4b5",
+ "navajowhite": "#ffdead",
+ "navy": "#000080",
+ "oldlace": "#fdf5e6",
+ "olive": "#808000",
+ "olivedrab": "#6b8e23",
+ "orange": "#ffa500",
+ "orangered": "#ff4500",
+ "orchid": "#da70d6",
+ "palegoldenrod": "#eee8aa",
+ "palegreen": "#98fb98",
+ "paleturquoise": "#afeeee",
+ "palevioletred": "#db7093",
+ "papayawhip": "#ffefd5",
+ "peachpuff": "#ffdab9",
+ "peru": "#cd853f",
+ "pink": "#ffc0cb",
+ "plum": "#dda0dd",
+ "powderblue": "#b0e0e6",
+ "purple": "#800080",
+ "rebeccapurple": "#663399",
+ "red": "#ff0000",
+ "rosybrown": "#bc8f8f",
+ "royalblue": "#4169e1",
+ "saddlebrown": "#8b4513",
+ "salmon": "#fa8072",
+ "sandybrown": "#f4a460",
+ "seagreen": "#2e8b57",
+ "seashell": "#fff5ee",
+ "sienna": "#a0522d",
+ "silver": "#c0c0c0",
+ "skyblue": "#87ceeb",
+ "slateblue": "#6a5acd",
+ "slategray": "#708090",
+ "slategrey": "#708090",
+ "snow": "#fffafa",
+ "springgreen": "#00ff7f",
+ "steelblue": "#4682b4",
+ "tan": "#d2b48c",
+ "teal": "#008080",
+ "thistle": "#d8bfd8",
+ "tomato": "#ff6347",
+ "turquoise": "#40e0d0",
+ "violet": "#ee82ee",
+ "wheat": "#f5deb3",
+ "white": "#ffffff",
+ "whitesmoke": "#f5f5f5",
+ "yellow": "#ffff00",
+ "yellowgreen": "#9acd32",
+}
diff --git a/venv/Lib/site-packages/PIL/ImageDraw.py b/venv/Lib/site-packages/PIL/ImageDraw.py
new file mode 100644
index 0000000..610ccd4
--- /dev/null
+++ b/venv/Lib/site-packages/PIL/ImageDraw.py
@@ -0,0 +1,1004 @@
+#
+# The Python Imaging Library
+# $Id$
+#
+# drawing interface operations
+#
+# History:
+# 1996-04-13 fl Created (experimental)
+# 1996-08-07 fl Filled polygons, ellipses.
+# 1996-08-13 fl Added text support
+# 1998-06-28 fl Handle I and F images
+# 1998-12-29 fl Added arc; use arc primitive to draw ellipses
+# 1999-01-10 fl Added shape stuff (experimental)
+# 1999-02-06 fl Added bitmap support
+# 1999-02-11 fl Changed all primitives to take options
+# 1999-02-20 fl Fixed backwards compatibility
+# 2000-10-12 fl Copy on write, when necessary
+# 2001-02-18 fl Use default ink for bitmap/text also in fill mode
+# 2002-10-24 fl Added support for CSS-style color strings
+# 2002-12-10 fl Added experimental support for RGBA-on-RGB drawing
+# 2002-12-11 fl Refactored low-level drawing API (work in progress)
+# 2004-08-26 fl Made Draw() a factory function, added getdraw() support
+# 2004-09-04 fl Added width support to line primitive
+# 2004-09-10 fl Added font mode handling
+# 2006-06-19 fl Added font bearing support (getmask2)
+#
+# Copyright (c) 1997-2006 by Secret Labs AB
+# Copyright (c) 1996-2006 by Fredrik Lundh
+#
+# See the README file for information on usage and redistribution.
+#
+
+import math
+import numbers
+
+from . import Image, ImageColor, ImageFont
+
+"""
+A simple 2D drawing interface for PIL images.
+
+
+# Pillow
+
+## Python Imaging Library (Fork)
+
+Pillow is the friendly PIL fork by [Alex Clark and
+Contributors](https://github.com/python-pillow/Pillow/graphs/contributors).
+PIL is the Python Imaging Library by Fredrik Lundh and Contributors.
+As of 2019, Pillow development is
+[supported by Tidelift](https://tidelift.com/subscription/pkg/pypi-pillow?utm_source=pypi-pillow&utm_medium=readme&utm_campaign=enterprise).
+
+
+
+
docs
+
+
+
+
+
+
tests
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
package
+
+
+
+
+
+
+
+
+
social
+
+
+
+
+
+
+
+## Overview
+
+The Python Imaging Library adds image processing capabilities to your Python interpreter.
+
+This library provides extensive file format support, an efficient internal representation, and fairly powerful image processing capabilities.
+
+The core image library is designed for fast access to data stored in a few basic pixel formats. It should provide a solid foundation for a general image processing tool.
+
+## More Information
+
+- [Documentation](https://pillow.readthedocs.io/)
+ - [Installation](https://pillow.readthedocs.io/en/latest/installation.html)
+ - [Handbook](https://pillow.readthedocs.io/en/latest/handbook/index.html)
+- [Contribute](https://github.com/python-pillow/Pillow/blob/main/.github/CONTRIBUTING.md)
+ - [Issues](https://github.com/python-pillow/Pillow/issues)
+ - [Pull requests](https://github.com/python-pillow/Pillow/pulls)
+- [Release notes](https://pillow.readthedocs.io/en/stable/releasenotes/index.html)
+- [Changelog](https://github.com/python-pillow/Pillow/blob/main/CHANGES.rst)
+ - [Pre-fork](https://github.com/python-pillow/Pillow/blob/main/CHANGES.rst#pre-fork)
+
+## Report a Vulnerability
+
+To report a security vulnerability, please follow the procedure described in the [Tidelift security policy](https://tidelift.com/docs/security).
+
+
diff --git a/venv/Lib/site-packages/Pillow-9.1.0.dist-info/RECORD b/venv/Lib/site-packages/Pillow-9.1.0.dist-info/RECORD
new file mode 100644
index 0000000..93f9a74
--- /dev/null
+++ b/venv/Lib/site-packages/Pillow-9.1.0.dist-info/RECORD
@@ -0,0 +1,210 @@
+PIL/BdfFontFile.py,sha256=hRnSgFZOIiTgWfJIaRHRQpU4TKVok2E31KJY6sbZPwc,2817
+PIL/BlpImagePlugin.py,sha256=SVs3I88sIWw7ibWRnAzDd9T-dIrWD6x00ZAf2HgNjh8,16143
+PIL/BmpImagePlugin.py,sha256=d9hGPxD0wjT_qhchHtiigxYLlFAGG61WcdUqEHqleTk,16252
+PIL/BufrStubImagePlugin.py,sha256=DE_t_ch4-YH_oimXYNMCefin4kcru6Uc2H_OTmwR6y4,1518
+PIL/ContainerIO.py,sha256=1U15zUXjWO8uWK-MyCp66Eh7djQEU-oUeCDoBqewNkA,2883
+PIL/CurImagePlugin.py,sha256=er_bI3V1Ezly0QfFJq0fZMlGwrD5izDutwF1FrOwiMA,1679
+PIL/DcxImagePlugin.py,sha256=bfESLTji9GerqI4oYsy5oTFyRMlr2mjSsXzpY9IuLsk,2145
+PIL/DdsImagePlugin.py,sha256=-sz60zvpuz89nyUobCPhdf-KWaT1yyeEa5PbRlxLMOw,8071
+PIL/EpsImagePlugin.py,sha256=qUxbQVsnzRyveDs9b8Co98sd0klsKr5GLCWtY3xbmB8,11949
+PIL/ExifTags.py,sha256=0YRoKyMwPabWOZZgVeLL6mlaGjbZgfF-z8WuUc6Ibb0,9446
+PIL/FitsImagePlugin.py,sha256=15BrvLXsw0F8WjBbP6-1RPHbJ4Lbd39OP4wkikcstC0,1971
+PIL/FitsStubImagePlugin.py,sha256=ETXbjvAFVMPfm51RNvPuo2B_pNRrJVuNNX-7-RmLUqw,1718
+PIL/FliImagePlugin.py,sha256=fR-Z9uY1udQu6FvzSqZJ3DAmIAaJUKsCNbO7OHN39cY,4239
+PIL/FontFile.py,sha256=LkQcbwUu1C4fokMnbg-ao9ksp2RX-saaPRie-z2rpH4,2765
+PIL/FpxImagePlugin.py,sha256=nKGioxa5C0q9X9qva3t_htRV_3jXQcFkclVxTEaSusk,6658
+PIL/FtexImagePlugin.py,sha256=TkvwTKeFRd1Qhcg6GyTBuFPI118MnegxC-JUoJMQVyY,4175
+PIL/GbrImagePlugin.py,sha256=K-olSg1M2bF2IofUeLABXfI1JLdrWsgiiU6yUTPhSWM,2795
+PIL/GdImageFile.py,sha256=JFWSUssG1z1r884GQtBbZ3T7uhPF4cDXSuW3ctgf3TU,2465
+PIL/GifImagePlugin.py,sha256=xA8QdF_rPNvr7Ceegl0I58FxsTxYaxuM4xpk9JoSZ3k,34458
+PIL/GimpGradientFile.py,sha256=G0ClRmjRHIJoU0nmG-P-tgehLHZip5i0rY4-5pjJ7bc,3353
+PIL/GimpPaletteFile.py,sha256=MGpf0WF_yTtMAXWvO_wlurgv_y80SX66EXprl6UIunM,1274
+PIL/GribStubImagePlugin.py,sha256=CocpZJIN8ckBtMQbq1VMA7NEKW5gwlzQ_mRZhHoyZho,1513
+PIL/Hdf5StubImagePlugin.py,sha256=FJ7-Vz1KY-DEOfrZg3cCMmG_wTa_qf6p41991P2Wfks,1515
+PIL/IcnsImagePlugin.py,sha256=x8JjanvXt_2BS-Qg8Jqt9XPsrCkhN2ESYGoKIoJ2WII,11755
+PIL/IcoImagePlugin.py,sha256=ZSfs8e9qJxIzcNUxuRC8S4KJgmvdH7KZOBuc70Ho9H0,11551
+PIL/ImImagePlugin.py,sha256=76DvUbRkFQ_DkEdthbApsuliNc5-FQHX3mnrYZdOkt4,10729
+PIL/Image.py,sha256=wQ34jHUxgvY6Udbz9u398kzpnEeZybBo5_ic8rQ4fZ0,125350
+PIL/ImageChops.py,sha256=HOGSnuU4EcCbdeUzEGPm54zewppHWWe12XLyOLLPgCw,7297
+PIL/ImageCms.py,sha256=MJHg18tKXzGIV0KZib3NQDIyaGI8XTJGIwzKoswfVbk,37951
+PIL/ImageColor.py,sha256=2e9xfO08S6afUzoahUIzyMN8RJcQsMz9E92rFnEhfP0,8727
+PIL/ImageDraw.py,sha256=rvMmVCjqAo_PRk41fOuOh3kkXYYTY8KinMvLkQ0RhO8,34710
+PIL/ImageDraw2.py,sha256=oBhpBTZhx3bd4D0s8E2kDjBzgThRkDU_TE_987l501k,5019
+PIL/ImageEnhance.py,sha256=CJnCouiBmxN2fE0xW7m_uMdBqcm-Fp0S3ruHhkygal4,3190
+PIL/ImageFile.py,sha256=_zfHA4FUsh2szsv0kd3LK1PywWNRH8krkvUVMi66DX8,22680
+PIL/ImageFilter.py,sha256=Sx99ij57imObeBdiR5w6cuhEG682SkfqtXx_vW7T_mk,16142
+PIL/ImageFont.py,sha256=TcCig_Hw5DbOnsZsWdLQeDEqplJotI2wG_Viw7P9W6o,45963
+PIL/ImageGrab.py,sha256=4W_qGYMJv7-5kWIvKnb3PzFMqdQERV32c43z-onj1CI,3823
+PIL/ImageMath.py,sha256=OsrEDBmoonjeOdcbuYQFEoU1sRT4sSCNO95EAq_CA_s,7253
+PIL/ImageMode.py,sha256=ZyTPlast0KeEp0-lbRcBoztKQzUY3FRaMAZza0Lm_mE,3006
+PIL/ImageMorph.py,sha256=KL2843wgfLyXPOWEJnTXRvySfbpRrlTqA_0M1j5xuD0,7773
+PIL/ImageOps.py,sha256=-MBNR_kztrdN6IAwTVXXHL2vvdO8ZkZjK-vMXVpmv5w,20504
+PIL/ImagePalette.py,sha256=rOpqcuH5DhJXPEvREna3Dg1N7ZK3TfnXHu5eZyltZTs,7841
+PIL/ImagePath.py,sha256=lVmH1-lCd0SyrFoqyhlstAFW2iJuC14fPcW8iewvxCQ,336
+PIL/ImageQt.py,sha256=hECe1rZpv1teaR5exrP39NbWBKwNGD7X5zoA5id_UJo,6698
+PIL/ImageSequence.py,sha256=3djA7vDH6wafTGbt4e_lPlVhy2TaKfdSrA1XQ4n-Uoc,1850
+PIL/ImageShow.py,sha256=Q_c_v9sy3wNnCnz7Ce1aM5vG1q74lFJ_ur6XvlNQXqc,12249
+PIL/ImageStat.py,sha256=Wdxu473_-bf3MeXLEj-9GrRftp6Ju_F7Sl_EKgzKd1Y,3899
+PIL/ImageTk.py,sha256=f6GGmApnpacVAHyOOVgG5PSLG6OCQInb5-2CSYfyTKg,9148
+PIL/ImageTransform.py,sha256=oO7Ir7j_5r4DeoZ-ZgqW9FO099cP2gHdE32SQdfmW_s,2883
+PIL/ImageWin.py,sha256=1MQBJS7tVrQzI9jN0nmeNeFpIaq8fXra9kQocHkiFxM,7191
+PIL/ImtImagePlugin.py,sha256=v_P09UT1Ae_HNUS-lTcMWfDTedfBDf-krhJRckDW6tg,2203
+PIL/IptcImagePlugin.py,sha256=-RZBUUodHcF5wLKanW1MxJj7cbLOpx5LvXqm0vDM22U,5714
+PIL/Jpeg2KImagePlugin.py,sha256=M8xsol1019D8hwtooNey-AGiNGaPPOqOat_0w4Tojaw,10455
+PIL/JpegImagePlugin.py,sha256=LRZGSeeoCbOyF3ISZp2VDYZGg5uL2JXLDf5AOCv3ghQ,28561
+PIL/JpegPresets.py,sha256=6nVnX_H8eA8ZO7AOVvkUx8gEN6QfI8zKnV6od16XgWE,12347
+PIL/McIdasImagePlugin.py,sha256=LrP5nA7l8IQG3WhlMI0Xs8fGXY_uf6IDmzNCERl3tGw,1754
+PIL/MicImagePlugin.py,sha256=Eh94vjTurXYkmm27hhooyNm9NkWWyVxP8Nq4thNLV6Y,2607
+PIL/MpegImagePlugin.py,sha256=n16Zgdy8Hcfke16lQwZWs53PZq4BA_OxPCMPDkW62nw,1803
+PIL/MpoImagePlugin.py,sha256=C-oosMx-C7dZT4QODBNYbX6LtfeEUxdpQ15Ychx9SuY,4478
+PIL/MspImagePlugin.py,sha256=ftTl14BpW1i3os_OUfusc7t4tRzBP4RrLxp76Sf9X4I,5527
+PIL/PSDraw.py,sha256=xmJ6GVUvDm1SC3QuUpYdeNfGu9lYBLX1ndCt96tObcc,6719
+PIL/PaletteFile.py,sha256=s3KtsDuY5S04MKDyiXK3iIbiOGzV9PvCDUpOQHI7yqc,1106
+PIL/PalmImagePlugin.py,sha256=lTVwwSPFrQ-IPFGU8_gRCMZ1Lb73cuVhQ-nkx1Q0oqc,9108
+PIL/PcdImagePlugin.py,sha256=cnBm_xKcpLGT6hZ8QKai9Up0gZERMxZwhDXl1hQtBm0,1476
+PIL/PcfFontFile.py,sha256=njhgblsjSVcITVz1DpWdEligmJgPMh5nTk_zDDWWTik,6348
+PIL/PcxImagePlugin.py,sha256=J-Pm2QBt5Hi4ObPeXDnc87X7nl1hbtTGqy4sTov6tug,5864
+PIL/PdfImagePlugin.py,sha256=f3foSWC1anwbnVBXVi-4wmtEnOR4_dbmqrbiQ--48Bk,7311
+PIL/PdfParser.py,sha256=Kxq4ZLMoayNODnpURMIcXljGJS-rX8AMBKA5iA0O29M,34561
+PIL/PixarImagePlugin.py,sha256=5MMcrrShVr511QKevK1ziKyJn0WllokWQxBhs8NWttY,1631
+PIL/PngImagePlugin.py,sha256=377uheEGeWvhlmTda0wRsQdVqAmOuboJWUMkHCl3Fs4,45016
+PIL/PpmImagePlugin.py,sha256=FclF4DGFyqWmqCOexRpzX47YuoylGNnVK1_VffYrP_s,5850
+PIL/PsdImagePlugin.py,sha256=8pYj9Sc4FYHl997QnJ6-79rAcS1flv7mIAMVR4_o1ws,7572
+PIL/PyAccess.py,sha256=SaGs2ZE4kjh-dybpAA5_Og4wuhA6d0LTPKK8t2aHffY,9607
+PIL/SgiImagePlugin.py,sha256=mqpi0G4aiKzWmJHk22WKZ0oGqsglcTNgDfp4H8S-GCM,6097
+PIL/SpiderImagePlugin.py,sha256=3weeJ7kc2t6gA-Hau9QdKgDdbXPcY8zrcTbR4cfAU-g,9554
+PIL/SunImagePlugin.py,sha256=bnjnVFRjvApCH1QC1F9HeynoCe5AZk3wa1tOhPvHzKU,4282
+PIL/TarIO.py,sha256=E_pjAxk9wHezXUuR_99liySBXfJoL2wjzdNDf0g1hTo,1440
+PIL/TgaImagePlugin.py,sha256=geeOJJJ-5Xz3u4JiDMrouyr-XFSqZ6Z48OuOaOY7_lI,6485
+PIL/TiffImagePlugin.py,sha256=uYKFj4zJivvZI_QSHRjR4uWJC_tHh4VgsegOAJPZCfY,75049
+PIL/TiffTags.py,sha256=CPaXv9s7T2oNFZFVbD-Kwz-K2V5ZcHKFkw3rT-Llkp4,15297
+PIL/WalImageFile.py,sha256=MhlGQBmSA_4OPBv6EL9bqFYe0YAf5rYtgAI_y0T920U,5520
+PIL/WebPImagePlugin.py,sha256=buw7FnrHviRmiYMcVSslJNohK3-OcwOUcnAkbZYJu-o,10924
+PIL/WmfImagePlugin.py,sha256=wvJeH9k4XJoUE2wVcf5G_8eeIuuO9BuGiV8jOZlcWrM,4625
+PIL/XVThumbImagePlugin.py,sha256=zmZ8Z4B8Kr6NOdUqSipW9_X5mKiLBLs-wxvPRRg1l0M,1940
+PIL/XbmImagePlugin.py,sha256=kuyd690rupwLFZj5r8hbGmI0Wr8sD_CceCuRew_PUew,2454
+PIL/XpmImagePlugin.py,sha256=1EBt-g678p0A0NXOkxq7sGM8dymneDMHHQmwJzAbrlw,3062
+PIL/__init__.py,sha256=3Z8lwq0danRE7WQFZxa7vMvfSjv_C4-Q73FUr_gHt4Y,1763
+PIL/__main__.py,sha256=axR7PO-HtXp-o0rBhKIxs0wark0rBfaDIhAIWqtWUo4,41
+PIL/__pycache__/BdfFontFile.cpython-39.pyc,,
+PIL/__pycache__/BlpImagePlugin.cpython-39.pyc,,
+PIL/__pycache__/BmpImagePlugin.cpython-39.pyc,,
+PIL/__pycache__/BufrStubImagePlugin.cpython-39.pyc,,
+PIL/__pycache__/ContainerIO.cpython-39.pyc,,
+PIL/__pycache__/CurImagePlugin.cpython-39.pyc,,
+PIL/__pycache__/DcxImagePlugin.cpython-39.pyc,,
+PIL/__pycache__/DdsImagePlugin.cpython-39.pyc,,
+PIL/__pycache__/EpsImagePlugin.cpython-39.pyc,,
+PIL/__pycache__/ExifTags.cpython-39.pyc,,
+PIL/__pycache__/FitsImagePlugin.cpython-39.pyc,,
+PIL/__pycache__/FitsStubImagePlugin.cpython-39.pyc,,
+PIL/__pycache__/FliImagePlugin.cpython-39.pyc,,
+PIL/__pycache__/FontFile.cpython-39.pyc,,
+PIL/__pycache__/FpxImagePlugin.cpython-39.pyc,,
+PIL/__pycache__/FtexImagePlugin.cpython-39.pyc,,
+PIL/__pycache__/GbrImagePlugin.cpython-39.pyc,,
+PIL/__pycache__/GdImageFile.cpython-39.pyc,,
+PIL/__pycache__/GifImagePlugin.cpython-39.pyc,,
+PIL/__pycache__/GimpGradientFile.cpython-39.pyc,,
+PIL/__pycache__/GimpPaletteFile.cpython-39.pyc,,
+PIL/__pycache__/GribStubImagePlugin.cpython-39.pyc,,
+PIL/__pycache__/Hdf5StubImagePlugin.cpython-39.pyc,,
+PIL/__pycache__/IcnsImagePlugin.cpython-39.pyc,,
+PIL/__pycache__/IcoImagePlugin.cpython-39.pyc,,
+PIL/__pycache__/ImImagePlugin.cpython-39.pyc,,
+PIL/__pycache__/Image.cpython-39.pyc,,
+PIL/__pycache__/ImageChops.cpython-39.pyc,,
+PIL/__pycache__/ImageCms.cpython-39.pyc,,
+PIL/__pycache__/ImageColor.cpython-39.pyc,,
+PIL/__pycache__/ImageDraw.cpython-39.pyc,,
+PIL/__pycache__/ImageDraw2.cpython-39.pyc,,
+PIL/__pycache__/ImageEnhance.cpython-39.pyc,,
+PIL/__pycache__/ImageFile.cpython-39.pyc,,
+PIL/__pycache__/ImageFilter.cpython-39.pyc,,
+PIL/__pycache__/ImageFont.cpython-39.pyc,,
+PIL/__pycache__/ImageGrab.cpython-39.pyc,,
+PIL/__pycache__/ImageMath.cpython-39.pyc,,
+PIL/__pycache__/ImageMode.cpython-39.pyc,,
+PIL/__pycache__/ImageMorph.cpython-39.pyc,,
+PIL/__pycache__/ImageOps.cpython-39.pyc,,
+PIL/__pycache__/ImagePalette.cpython-39.pyc,,
+PIL/__pycache__/ImagePath.cpython-39.pyc,,
+PIL/__pycache__/ImageQt.cpython-39.pyc,,
+PIL/__pycache__/ImageSequence.cpython-39.pyc,,
+PIL/__pycache__/ImageShow.cpython-39.pyc,,
+PIL/__pycache__/ImageStat.cpython-39.pyc,,
+PIL/__pycache__/ImageTk.cpython-39.pyc,,
+PIL/__pycache__/ImageTransform.cpython-39.pyc,,
+PIL/__pycache__/ImageWin.cpython-39.pyc,,
+PIL/__pycache__/ImtImagePlugin.cpython-39.pyc,,
+PIL/__pycache__/IptcImagePlugin.cpython-39.pyc,,
+PIL/__pycache__/Jpeg2KImagePlugin.cpython-39.pyc,,
+PIL/__pycache__/JpegImagePlugin.cpython-39.pyc,,
+PIL/__pycache__/JpegPresets.cpython-39.pyc,,
+PIL/__pycache__/McIdasImagePlugin.cpython-39.pyc,,
+PIL/__pycache__/MicImagePlugin.cpython-39.pyc,,
+PIL/__pycache__/MpegImagePlugin.cpython-39.pyc,,
+PIL/__pycache__/MpoImagePlugin.cpython-39.pyc,,
+PIL/__pycache__/MspImagePlugin.cpython-39.pyc,,
+PIL/__pycache__/PSDraw.cpython-39.pyc,,
+PIL/__pycache__/PaletteFile.cpython-39.pyc,,
+PIL/__pycache__/PalmImagePlugin.cpython-39.pyc,,
+PIL/__pycache__/PcdImagePlugin.cpython-39.pyc,,
+PIL/__pycache__/PcfFontFile.cpython-39.pyc,,
+PIL/__pycache__/PcxImagePlugin.cpython-39.pyc,,
+PIL/__pycache__/PdfImagePlugin.cpython-39.pyc,,
+PIL/__pycache__/PdfParser.cpython-39.pyc,,
+PIL/__pycache__/PixarImagePlugin.cpython-39.pyc,,
+PIL/__pycache__/PngImagePlugin.cpython-39.pyc,,
+PIL/__pycache__/PpmImagePlugin.cpython-39.pyc,,
+PIL/__pycache__/PsdImagePlugin.cpython-39.pyc,,
+PIL/__pycache__/PyAccess.cpython-39.pyc,,
+PIL/__pycache__/SgiImagePlugin.cpython-39.pyc,,
+PIL/__pycache__/SpiderImagePlugin.cpython-39.pyc,,
+PIL/__pycache__/SunImagePlugin.cpython-39.pyc,,
+PIL/__pycache__/TarIO.cpython-39.pyc,,
+PIL/__pycache__/TgaImagePlugin.cpython-39.pyc,,
+PIL/__pycache__/TiffImagePlugin.cpython-39.pyc,,
+PIL/__pycache__/TiffTags.cpython-39.pyc,,
+PIL/__pycache__/WalImageFile.cpython-39.pyc,,
+PIL/__pycache__/WebPImagePlugin.cpython-39.pyc,,
+PIL/__pycache__/WmfImagePlugin.cpython-39.pyc,,
+PIL/__pycache__/XVThumbImagePlugin.cpython-39.pyc,,
+PIL/__pycache__/XbmImagePlugin.cpython-39.pyc,,
+PIL/__pycache__/XpmImagePlugin.cpython-39.pyc,,
+PIL/__pycache__/__init__.cpython-39.pyc,,
+PIL/__pycache__/__main__.cpython-39.pyc,,
+PIL/__pycache__/_binary.cpython-39.pyc,,
+PIL/__pycache__/_tkinter_finder.cpython-39.pyc,,
+PIL/__pycache__/_util.cpython-39.pyc,,
+PIL/__pycache__/_version.cpython-39.pyc,,
+PIL/__pycache__/features.cpython-39.pyc,,
+PIL/_binary.py,sha256=E5qhxNJ7hhbEoqu0mODOXHT8z-FDRShXG3jTJhsDdas,2043
+PIL/_imaging.cp39-win_amd64.pyd,sha256=rjBx1Em4OZEdaaB5EUE6ptH7zLbSBIHA54FxMdfCieM,3202048
+PIL/_imagingcms.cp39-win_amd64.pyd,sha256=T63FsiwkHW_aOGKZGm34AagOcdC2EyXluthlmv9f9iU,254976
+PIL/_imagingft.cp39-win_amd64.pyd,sha256=oudclQKBA5NhGs5sY2hVGBwtUsw81gkspvMS4QIhxkA,1505280
+PIL/_imagingmath.cp39-win_amd64.pyd,sha256=lSTNqJ-7fsT0uxDXrWisXD0H0ReS3r4XzoSTswIBbNk,25088
+PIL/_imagingmorph.cp39-win_amd64.pyd,sha256=PSQSThxHCz0soHLNAgh83gJyVfGtPSKxHLg_FbYCR4g,13824
+PIL/_imagingtk.cp39-win_amd64.pyd,sha256=kd8eB3Yid40OlxZToaIFyVZqDKyCbLn94wxfG22CUXs,15360
+PIL/_tkinter_finder.py,sha256=_h4IyntUxL3ZCMnuKGxvW5VwN9k8Yiel0E4j_i41nxk,752
+PIL/_util.py,sha256=pbjX5KY1W2oZyYVC4TE9ai2PfrJZrAsO5hAnz_JMees,359
+PIL/_version.py,sha256=TSDtIA_HTVdlnbVHs_Qn2GMMtftmiSHLYUnuWLa1_rk,50
+PIL/_webp.cp39-win_amd64.pyd,sha256=LdSJ36RBE2X0JvsZn5_DyMuhpgYf_gJGVKX2FROtna8,522752
+PIL/concrt140.dll,sha256=VzIpoH84q50vwuGluY6SQ7mzkQAyMYDIOtfdr5ju5Go,317864
+PIL/features.py,sha256=j2LT6v78cHWbR8z8OVaAGIbJWI-Bs62pfiB1i1fminM,9387
+PIL/msvcp140.dll,sha256=n-5vNlR9b26nygM4ZVVV26a7D3mLxgM00puU0VR9pNo,566704
+PIL/msvcp140_1.dll,sha256=hzGpPlGcJZXJ_UiebZrAfpZESMDaHI7p7lAKeYlIJhc,23944
+PIL/msvcp140_2.dll,sha256=Nmr44HHwBNpdlagypGsuiCGo4ClDQKk_fJXPSMRBBn4,186800
+PIL/msvcp140_atomic_wait.dll,sha256=xhoocR-Mbpv9SHnPX1OwE9ZTutrTCKvj6IfGlLIj1vA,57264
+PIL/msvcp140_codecvt_ids.dll,sha256=m0X9BpvQB22Kv-t8PDCh9cX8jnEkAXhTqT2DGjRsPSE,21424
+PIL/vccorlib140.dll,sha256=eShGWb9DAhYjAnN9JROxfgl0LN77lUDoD5fTDJMHfXw,335792
+PIL/vcruntime140.dll,sha256=nStA8DlcxdG01eoXuElwwplx1EjDcQRnbbV3WG1K0bE,98224
+PIL/vcruntime140_1.dll,sha256=NASKuqBw7ME7MYzqMUJfTKPt0TPTUDGKxlJZ5gWMizI,37256
+Pillow-9.1.0.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
+Pillow-9.1.0.dist-info/LICENSE,sha256=plVMtze6bJtH0zAfeN4DtO0NPwjWz5QAcU89TIlPaUM,1444
+Pillow-9.1.0.dist-info/METADATA,sha256=DLY0qLsMqaQS2OR2dtHBPI2XrMvxgvmrahFZ5Bdnq4U,8722
+Pillow-9.1.0.dist-info/RECORD,,
+Pillow-9.1.0.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
+Pillow-9.1.0.dist-info/WHEEL,sha256=fVcVlLzi8CGi_Ul8vjMdn8gER25dn5GBg9E6k9z41-Y,100
+Pillow-9.1.0.dist-info/top_level.txt,sha256=riZqrk-hyZqh5f1Z0Zwii3dKfxEsByhu9cU9IODF-NY,4
+Pillow-9.1.0.dist-info/zip-safe,sha256=frcCV1k9oG9oKj3dpUqdJg1PxRT2RSN_XKdLCPjaYaY,2
diff --git a/venv/Lib/site-packages/Pillow-9.1.0.dist-info/REQUESTED b/venv/Lib/site-packages/Pillow-9.1.0.dist-info/REQUESTED
new file mode 100644
index 0000000..e69de29
diff --git a/venv/Lib/site-packages/Pillow-9.1.0.dist-info/WHEEL b/venv/Lib/site-packages/Pillow-9.1.0.dist-info/WHEEL
new file mode 100644
index 0000000..d5a9837
--- /dev/null
+++ b/venv/Lib/site-packages/Pillow-9.1.0.dist-info/WHEEL
@@ -0,0 +1,5 @@
+Wheel-Version: 1.0
+Generator: bdist_wheel (0.37.1)
+Root-Is-Purelib: false
+Tag: cp39-cp39-win_amd64
+
diff --git a/venv/Lib/site-packages/Pillow-9.1.0.dist-info/top_level.txt b/venv/Lib/site-packages/Pillow-9.1.0.dist-info/top_level.txt
new file mode 100644
index 0000000..b338169
--- /dev/null
+++ b/venv/Lib/site-packages/Pillow-9.1.0.dist-info/top_level.txt
@@ -0,0 +1 @@
+PIL
diff --git a/venv/Lib/site-packages/Pillow-9.1.0.dist-info/zip-safe b/venv/Lib/site-packages/Pillow-9.1.0.dist-info/zip-safe
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/venv/Lib/site-packages/Pillow-9.1.0.dist-info/zip-safe
@@ -0,0 +1 @@
+
diff --git a/venv/Lib/site-packages/backports/configparser/__init__.py b/venv/Lib/site-packages/backports/configparser/__init__.py
new file mode 100644
index 0000000..73386a9
--- /dev/null
+++ b/venv/Lib/site-packages/backports/configparser/__init__.py
@@ -0,0 +1,1440 @@
+"""Configuration file parser.
+
+A configuration file consists of sections, lead by a "[section]" header,
+and followed by "name: value" entries, with continuations and such in
+the style of RFC 822.
+
+Intrinsic defaults can be specified by passing them into the
+ConfigParser constructor as a dictionary.
+
+class:
+
+ConfigParser -- responsible for parsing a list of
+ configuration files, and managing the parsed database.
+
+ methods:
+
+ __init__(defaults=None, dict_type=_default_dict, allow_no_value=False,
+ delimiters=('=', ':'), comment_prefixes=('#', ';'),
+ inline_comment_prefixes=None, strict=True,
+ empty_lines_in_values=True, default_section='DEFAULT',
+ interpolation=, converters=):
+ Create the parser. When `defaults' is given, it is initialized into the
+ dictionary or intrinsic defaults. The keys must be strings, the values
+ must be appropriate for %()s string interpolation.
+
+ When `dict_type' is given, it will be used to create the dictionary
+ objects for the list of sections, for the options within a section, and
+ for the default values.
+
+ When `delimiters' is given, it will be used as the set of substrings
+ that divide keys from values.
+
+ When `comment_prefixes' is given, it will be used as the set of
+ substrings that prefix comments in empty lines. Comments can be
+ indented.
+
+ When `inline_comment_prefixes' is given, it will be used as the set of
+ substrings that prefix comments in non-empty lines.
+
+ When `strict` is True, the parser won't allow for any section or option
+ duplicates while reading from a single source (file, string or
+ dictionary). Default is True.
+
+ When `empty_lines_in_values' is False (default: True), each empty line
+ marks the end of an option. Otherwise, internal empty lines of
+ a multiline option are kept as part of the value.
+
+ When `allow_no_value' is True (default: False), options without
+ values are accepted; the value presented for these is None.
+
+ When `default_section' is given, the name of the special section is
+ named accordingly. By default it is called ``"DEFAULT"`` but this can
+ be customized to point to any other valid section name. Its current
+ value can be retrieved using the ``parser_instance.default_section``
+ attribute and may be modified at runtime.
+
+ When `interpolation` is given, it should be an Interpolation subclass
+ instance. It will be used as the handler for option value
+ pre-processing when using getters. RawConfigParser objects don't do
+ any sort of interpolation, whereas ConfigParser uses an instance of
+ BasicInterpolation. The library also provides a ``zc.buildbot``
+ inspired ExtendedInterpolation implementation.
+
+ When `converters` is given, it should be a dictionary where each key
+ represents the name of a type converter and each value is a callable
+ implementing the conversion from string to the desired datatype. Every
+ converter gets its corresponding get*() method on the parser object and
+ section proxies.
+
+ sections()
+ Return all the configuration section names, sans DEFAULT.
+
+ has_section(section)
+ Return whether the given section exists.
+
+ has_option(section, option)
+ Return whether the given option exists in the given section.
+
+ options(section)
+ Return list of configuration options for the named section.
+
+ read(filenames, encoding=None)
+ Read and parse the iterable of named configuration files, given by
+ name. A single filename is also allowed. Non-existing files
+ are ignored. Return list of successfully read files.
+
+ read_file(f, filename=None)
+ Read and parse one configuration file, given as a file object.
+ The filename defaults to f.name; it is only used in error
+ messages (if f has no `name' attribute, the string `??>' is used).
+
+ read_string(string)
+ Read configuration from a given string.
+
+ read_dict(dictionary)
+ Read configuration from a dictionary. Keys are section names,
+ values are dictionaries with keys and values that should be present
+ in the section. If the used dictionary type preserves order, sections
+ and their keys will be added in order. Values are automatically
+ converted to strings.
+
+ get(section, option, raw=False, vars=None, fallback=_UNSET)
+ Return a string value for the named option. All % interpolations are
+ expanded in the return values, based on the defaults passed into the
+ constructor and the DEFAULT section. Additional substitutions may be
+ provided using the `vars' argument, which must be a dictionary whose
+ contents override any pre-existing defaults. If `option' is a key in
+ `vars', the value from `vars' is used.
+
+ getint(section, options, raw=False, vars=None, fallback=_UNSET)
+ Like get(), but convert value to an integer.
+
+ getfloat(section, options, raw=False, vars=None, fallback=_UNSET)
+ Like get(), but convert value to a float.
+
+ getboolean(section, options, raw=False, vars=None, fallback=_UNSET)
+ Like get(), but convert value to a boolean (currently case
+ insensitively defined as 0, false, no, off for False, and 1, true,
+ yes, on for True). Returns False or True.
+
+ items(section=_UNSET, raw=False, vars=None)
+ If section is given, return a list of tuples with (name, value) for
+ each option in the section. Otherwise, return a list of tuples with
+ (section_name, section_proxy) for each section, including DEFAULTSECT.
+
+ remove_section(section)
+ Remove the given file section and all its options.
+
+ remove_option(section, option)
+ Remove the given option from the given section.
+
+ set(section, option, value)
+ Set the given option.
+
+ write(fp, space_around_delimiters=True)
+ Write the configuration state in .ini format. If
+ `space_around_delimiters' is True (the default), delimiters
+ between keys and values are surrounded by spaces.
+"""
+
+from collections.abc import MutableMapping
+from collections import ChainMap as _ChainMap
+import functools
+from .compat import io
+import itertools
+import os
+import re
+import sys
+import warnings
+
+
+__all__ = [
+ "NoSectionError",
+ "DuplicateOptionError",
+ "DuplicateSectionError",
+ "NoOptionError",
+ "InterpolationError",
+ "InterpolationDepthError",
+ "InterpolationMissingOptionError",
+ "InterpolationSyntaxError",
+ "ParsingError",
+ "MissingSectionHeaderError",
+ "ConfigParser",
+ "SafeConfigParser",
+ "RawConfigParser",
+ "Interpolation",
+ "BasicInterpolation",
+ "ExtendedInterpolation",
+ "LegacyInterpolation",
+ "SectionProxy",
+ "ConverterMapping",
+ "DEFAULTSECT",
+ "MAX_INTERPOLATION_DEPTH",
+]
+
+_default_dict = dict
+DEFAULTSECT = "DEFAULT"
+
+MAX_INTERPOLATION_DEPTH = 10
+
+
+# exception classes
+class Error(Exception):
+ """Base class for ConfigParser exceptions."""
+
+ def __init__(self, msg=''):
+ self.message = msg
+ Exception.__init__(self, msg)
+
+ def __repr__(self):
+ return self.message
+
+ __str__ = __repr__
+
+
+class NoSectionError(Error):
+ """Raised when no section matches a requested option."""
+
+ def __init__(self, section):
+ Error.__init__(self, 'No section: %r' % (section,))
+ self.section = section
+ self.args = (section,)
+
+
+class DuplicateSectionError(Error):
+ """Raised when a section is repeated in an input source.
+
+ Possible repetitions that raise this exception are: multiple creation
+ using the API or in strict parsers when a section is found more than once
+ in a single input file, string or dictionary.
+ """
+
+ def __init__(self, section, source=None, lineno=None):
+ msg = [repr(section), " already exists"]
+ if source is not None:
+ message = ["While reading from ", repr(source)]
+ if lineno is not None:
+ message.append(" [line {0:2d}]".format(lineno))
+ message.append(": section ")
+ message.extend(msg)
+ msg = message
+ else:
+ msg.insert(0, "Section ")
+ Error.__init__(self, "".join(msg))
+ self.section = section
+ self.source = source
+ self.lineno = lineno
+ self.args = (section, source, lineno)
+
+
+class DuplicateOptionError(Error):
+ """Raised by strict parsers when an option is repeated in an input source.
+
+ Current implementation raises this exception only when an option is found
+ more than once in a single file, string or dictionary.
+ """
+
+ def __init__(self, section, option, source=None, lineno=None):
+ msg = [repr(option), " in section ", repr(section), " already exists"]
+ if source is not None:
+ message = ["While reading from ", repr(source)]
+ if lineno is not None:
+ message.append(" [line {0:2d}]".format(lineno))
+ message.append(": option ")
+ message.extend(msg)
+ msg = message
+ else:
+ msg.insert(0, "Option ")
+ Error.__init__(self, "".join(msg))
+ self.section = section
+ self.option = option
+ self.source = source
+ self.lineno = lineno
+ self.args = (section, option, source, lineno)
+
+
+class NoOptionError(Error):
+ """A requested option was not found."""
+
+ def __init__(self, option, section):
+ Error.__init__(self, "No option %r in section: %r" % (option, section))
+ self.option = option
+ self.section = section
+ self.args = (option, section)
+
+
+class InterpolationError(Error):
+ """Base class for interpolation-related exceptions."""
+
+ def __init__(self, option, section, msg):
+ Error.__init__(self, msg)
+ self.option = option
+ self.section = section
+ self.args = (option, section, msg)
+
+
+class InterpolationMissingOptionError(InterpolationError):
+ """A string substitution required a setting which was not available."""
+
+ def __init__(self, option, section, rawval, reference):
+ msg = (
+ "Bad value substitution: option {!r} in section {!r} contains "
+ "an interpolation key {!r} which is not a valid option name. "
+ "Raw value: {!r}".format(option, section, reference, rawval)
+ )
+ InterpolationError.__init__(self, option, section, msg)
+ self.reference = reference
+ self.args = (option, section, rawval, reference)
+
+
+class InterpolationSyntaxError(InterpolationError):
+ """Raised when the source text contains invalid syntax.
+
+ Current implementation raises this exception when the source text into
+ which substitutions are made does not conform to the required syntax.
+ """
+
+
+class InterpolationDepthError(InterpolationError):
+ """Raised when substitutions are nested too deeply."""
+
+ def __init__(self, option, section, rawval):
+ msg = (
+ "Recursion limit exceeded in value substitution: option {!r} "
+ "in section {!r} contains an interpolation key which "
+ "cannot be substituted in {} steps. Raw value: {!r}"
+ "".format(option, section, MAX_INTERPOLATION_DEPTH, rawval)
+ )
+ InterpolationError.__init__(self, option, section, msg)
+ self.args = (option, section, rawval)
+
+
+class ParsingError(Error):
+ """Raised when a configuration file does not follow legal syntax."""
+
+ def __init__(self, source=None, filename=None):
+ # Exactly one of `source'/`filename' arguments has to be given.
+ # `filename' kept for compatibility.
+ if filename and source:
+ raise ValueError(
+ "Cannot specify both `filename' and `source'. " "Use `source'."
+ )
+ elif not filename and not source:
+ raise ValueError("Required argument `source' not given.")
+ elif filename:
+ source = filename
+ Error.__init__(self, 'Source contains parsing errors: %r' % source)
+ self.source = source
+ self.errors = []
+ self.args = (source,)
+
+ @property
+ def filename(self):
+ """Deprecated, use `source'."""
+ warnings.warn(
+ "The 'filename' attribute will be removed in future versions. "
+ "Use 'source' instead.",
+ DeprecationWarning,
+ stacklevel=2,
+ )
+ return self.source
+
+ @filename.setter
+ def filename(self, value):
+ """Deprecated, user `source'."""
+ warnings.warn(
+ "The 'filename' attribute will be removed in future versions. "
+ "Use 'source' instead.",
+ DeprecationWarning,
+ stacklevel=2,
+ )
+ self.source = value
+
+ def append(self, lineno, line):
+ self.errors.append((lineno, line))
+ self.message += '\n\t[line %2d]: %s' % (lineno, line)
+
+
+class MissingSectionHeaderError(ParsingError):
+ """Raised when a key-value pair is found before any section header."""
+
+ def __init__(self, filename, lineno, line):
+ Error.__init__(
+ self,
+ 'File contains no section headers.\nfile: %r, line: %d\n%r'
+ % (filename, lineno, line),
+ )
+ self.source = filename
+ self.lineno = lineno
+ self.line = line
+ self.args = (filename, lineno, line)
+
+
+# Used in parser getters to indicate the default behaviour when a specific
+# option is not found it to raise an exception. Created to enable `None' as
+# a valid fallback value.
+_UNSET = object()
+
+
+class Interpolation:
+ """Dummy interpolation that passes the value through with no changes."""
+
+ def before_get(self, parser, section, option, value, defaults):
+ return value
+
+ def before_set(self, parser, section, option, value):
+ return value
+
+ def before_read(self, parser, section, option, value):
+ return value
+
+ def before_write(self, parser, section, option, value):
+ return value
+
+
+class BasicInterpolation(Interpolation):
+ """Interpolation as implemented in the classic ConfigParser.
+
+ The option values can contain format strings which refer to other values in
+ the same section, or values in the special default section.
+
+ For example:
+
+ something: %(dir)s/whatever
+
+ would resolve the "%(dir)s" to the value of dir. All reference
+ expansions are done late, on demand. If a user needs to use a bare % in
+ a configuration file, she can escape it by writing %%. Other % usage
+ is considered a user error and raises `InterpolationSyntaxError'."""
+
+ _KEYCRE = re.compile(r"%\(([^)]+)\)s")
+
+ def before_get(self, parser, section, option, value, defaults):
+ L = []
+ self._interpolate_some(parser, option, L, value, section, defaults, 1)
+ return ''.join(L)
+
+ def before_set(self, parser, section, option, value):
+ tmp_value = value.replace('%%', '') # escaped percent signs
+ tmp_value = self._KEYCRE.sub('', tmp_value) # valid syntax
+ if '%' in tmp_value:
+ raise ValueError(
+ "invalid interpolation syntax in %r at "
+ "position %d" % (value, tmp_value.find('%'))
+ )
+ return value
+
+ def _interpolate_some( # noqa: C901
+ self, parser, option, accum, rest, section, map, depth
+ ):
+ rawval = parser.get(section, option, raw=True, fallback=rest)
+ if depth > MAX_INTERPOLATION_DEPTH:
+ raise InterpolationDepthError(option, section, rawval)
+ while rest:
+ p = rest.find("%")
+ if p < 0:
+ accum.append(rest)
+ return
+ if p > 0:
+ accum.append(rest[:p])
+ rest = rest[p:]
+ # p is no longer used
+ c = rest[1:2]
+ if c == "%":
+ accum.append("%")
+ rest = rest[2:]
+ elif c == "(":
+ m = self._KEYCRE.match(rest)
+ if m is None:
+ raise InterpolationSyntaxError(
+ option,
+ section,
+ "bad interpolation variable reference %r" % rest,
+ )
+ var = parser.optionxform(m.group(1))
+ rest = rest[m.end() :]
+ try:
+ v = map[var]
+ except KeyError:
+ raise InterpolationMissingOptionError(
+ option, section, rawval, var
+ ) from None
+ if "%" in v:
+ self._interpolate_some(
+ parser, option, accum, v, section, map, depth + 1
+ )
+ else:
+ accum.append(v)
+ else:
+ raise InterpolationSyntaxError(
+ option,
+ section,
+ "'%%' must be followed by '%%' or '(', " "found: %r" % (rest,),
+ )
+
+
+class ExtendedInterpolation(Interpolation):
+ """Advanced variant of interpolation, supports the syntax used by
+ `zc.buildout'. Enables interpolation between sections."""
+
+ _KEYCRE = re.compile(r"\$\{([^}]+)\}")
+
+ def before_get(self, parser, section, option, value, defaults):
+ L = []
+ self._interpolate_some(parser, option, L, value, section, defaults, 1)
+ return ''.join(L)
+
+ def before_set(self, parser, section, option, value):
+ tmp_value = value.replace('$$', '') # escaped dollar signs
+ tmp_value = self._KEYCRE.sub('', tmp_value) # valid syntax
+ if '$' in tmp_value:
+ raise ValueError(
+ "invalid interpolation syntax in %r at "
+ "position %d" % (value, tmp_value.find('$'))
+ )
+ return value
+
+ def _interpolate_some( # noqa: C901
+ self, parser, option, accum, rest, section, map, depth
+ ):
+ rawval = parser.get(section, option, raw=True, fallback=rest)
+ if depth > MAX_INTERPOLATION_DEPTH:
+ raise InterpolationDepthError(option, section, rawval)
+ while rest:
+ p = rest.find("$")
+ if p < 0:
+ accum.append(rest)
+ return
+ if p > 0:
+ accum.append(rest[:p])
+ rest = rest[p:]
+ # p is no longer used
+ c = rest[1:2]
+ if c == "$":
+ accum.append("$")
+ rest = rest[2:]
+ elif c == "{":
+ m = self._KEYCRE.match(rest)
+ if m is None:
+ raise InterpolationSyntaxError(
+ option,
+ section,
+ "bad interpolation variable reference %r" % rest,
+ )
+ path = m.group(1).split(':')
+ rest = rest[m.end() :]
+ sect = section
+ opt = option
+ try:
+ if len(path) == 1:
+ opt = parser.optionxform(path[0])
+ v = map[opt]
+ elif len(path) == 2:
+ sect = path[0]
+ opt = parser.optionxform(path[1])
+ v = parser.get(sect, opt, raw=True)
+ else:
+ raise InterpolationSyntaxError(
+ option, section, "More than one ':' found: %r" % (rest,)
+ )
+ except (KeyError, NoSectionError, NoOptionError):
+ raise InterpolationMissingOptionError(
+ option, section, rawval, ":".join(path)
+ ) from None
+ if "$" in v:
+ self._interpolate_some(
+ parser,
+ opt,
+ accum,
+ v,
+ sect,
+ dict(parser.items(sect, raw=True)),
+ depth + 1,
+ )
+ else:
+ accum.append(v)
+ else:
+ raise InterpolationSyntaxError(
+ option,
+ section,
+ "'$' must be followed by '$' or '{', " "found: %r" % (rest,),
+ )
+
+
+class LegacyInterpolation(Interpolation):
+ """Deprecated interpolation used in old versions of ConfigParser.
+ Use BasicInterpolation or ExtendedInterpolation instead."""
+
+ _KEYCRE = re.compile(r"%\(([^)]*)\)s|.")
+
+ def before_get(self, parser, section, option, value, vars):
+ rawval = value
+ depth = MAX_INTERPOLATION_DEPTH
+ while depth: # Loop through this until it's done
+ depth -= 1
+ if value and "%(" in value:
+ replace = functools.partial(self._interpolation_replace, parser=parser)
+ value = self._KEYCRE.sub(replace, value)
+ try:
+ value = value % vars
+ except KeyError as e:
+ raise InterpolationMissingOptionError(
+ option, section, rawval, e.args[0]
+ ) from None
+ else:
+ break
+ if value and "%(" in value:
+ raise InterpolationDepthError(option, section, rawval)
+ return value
+
+ def before_set(self, parser, section, option, value):
+ return value
+
+ @staticmethod
+ def _interpolation_replace(match, parser):
+ s = match.group(1)
+ if s is None:
+ return match.group()
+ else:
+ return "%%(%s)s" % parser.optionxform(s)
+
+
+class RawConfigParser(MutableMapping):
+ """ConfigParser that does not do interpolation."""
+
+ # Regular expressions for parsing section headers and options
+ _SECT_TMPL = r"""
+ \[ # [
+ (?P.+) # very permissive!
+ \] # ]
+ """
+ _OPT_TMPL = r"""
+ (?P
.*?) # very permissive!
+ \s*(?P{delim})\s* # any number of space/tab,
+ # followed by any of the
+ # allowed delimiters,
+ # followed by any space/tab
+ (?P.*)$ # everything up to eol
+ """
+ _OPT_NV_TMPL = r"""
+ (?P
.*?) # very permissive!
+ \s*(?: # any number of space/tab,
+ (?P{delim})\s* # optionally followed by
+ # any of the allowed
+ # delimiters, followed by any
+ # space/tab
+ (?P.*))?$ # everything up to eol
+ """
+ # Interpolation algorithm to be used if the user does not specify another
+ _DEFAULT_INTERPOLATION = Interpolation()
+ # Compiled regular expression for matching sections
+ SECTCRE = re.compile(_SECT_TMPL, re.VERBOSE)
+ # Compiled regular expression for matching options with typical separators
+ OPTCRE = re.compile(_OPT_TMPL.format(delim="=|:"), re.VERBOSE)
+ # Compiled regular expression for matching options with optional values
+ # delimited using typical separators
+ OPTCRE_NV = re.compile(_OPT_NV_TMPL.format(delim="=|:"), re.VERBOSE)
+ # Compiled regular expression for matching leading whitespace in a line
+ NONSPACECRE = re.compile(r"\S")
+ # Possible boolean values in the configuration.
+ BOOLEAN_STATES = {
+ '1': True,
+ 'yes': True,
+ 'true': True,
+ 'on': True,
+ '0': False,
+ 'no': False,
+ 'false': False,
+ 'off': False,
+ }
+
+ def __init__(
+ self,
+ defaults=None,
+ dict_type=_default_dict,
+ allow_no_value=False,
+ *,
+ delimiters=('=', ':'),
+ comment_prefixes=('#', ';'),
+ inline_comment_prefixes=None,
+ strict=True,
+ empty_lines_in_values=True,
+ default_section=DEFAULTSECT,
+ interpolation=_UNSET,
+ converters=_UNSET,
+ ):
+
+ self._dict = dict_type
+ self._sections = self._dict()
+ self._defaults = self._dict()
+ self._converters = ConverterMapping(self)
+ self._proxies = self._dict()
+ self._proxies[default_section] = SectionProxy(self, default_section)
+ self._delimiters = tuple(delimiters)
+ if delimiters == ('=', ':'):
+ self._optcre = self.OPTCRE_NV if allow_no_value else self.OPTCRE
+ else:
+ d = "|".join(re.escape(d) for d in delimiters)
+ if allow_no_value:
+ self._optcre = re.compile(self._OPT_NV_TMPL.format(delim=d), re.VERBOSE)
+ else:
+ self._optcre = re.compile(self._OPT_TMPL.format(delim=d), re.VERBOSE)
+ self._comment_prefixes = tuple(comment_prefixes or ())
+ self._inline_comment_prefixes = tuple(inline_comment_prefixes or ())
+ self._strict = strict
+ self._allow_no_value = allow_no_value
+ self._empty_lines_in_values = empty_lines_in_values
+ self.default_section = default_section
+ self._interpolation = interpolation
+ if self._interpolation is _UNSET:
+ self._interpolation = self._DEFAULT_INTERPOLATION
+ if self._interpolation is None:
+ self._interpolation = Interpolation()
+ if converters is not _UNSET:
+ self._converters.update(converters)
+ if defaults:
+ self._read_defaults(defaults)
+
+ def defaults(self):
+ return self._defaults
+
+ def sections(self):
+ """Return a list of section names, excluding [DEFAULT]"""
+ # self._sections will never have [DEFAULT] in it
+ return list(self._sections.keys())
+
+ def add_section(self, section):
+ """Create a new section in the configuration.
+
+ Raise DuplicateSectionError if a section by the specified name
+ already exists. Raise ValueError if name is DEFAULT.
+ """
+ if section == self.default_section:
+ raise ValueError('Invalid section name: %r' % section)
+
+ if section in self._sections:
+ raise DuplicateSectionError(section)
+ self._sections[section] = self._dict()
+ self._proxies[section] = SectionProxy(self, section)
+
+ def has_section(self, section):
+ """Indicate whether the named section is present in the configuration.
+
+ The DEFAULT section is not acknowledged.
+ """
+ return section in self._sections
+
+ def options(self, section):
+ """Return a list of option names for the given section name."""
+ try:
+ opts = self._sections[section].copy()
+ except KeyError:
+ raise NoSectionError(section) from None
+ opts.update(self._defaults)
+ return list(opts.keys())
+
+ def read(self, filenames, encoding=None):
+ """Read and parse a filename or an iterable of filenames.
+
+ Files that cannot be opened are silently ignored; this is
+ designed so that you can specify an iterable of potential
+ configuration file locations (e.g. current directory, user's
+ home directory, systemwide directory), and all existing
+ configuration files in the iterable will be read. A single
+ filename may also be given.
+
+ Return list of successfully read files.
+ """
+ if isinstance(filenames, (str, bytes, os.PathLike)):
+ filenames = [filenames]
+ encoding = io.text_encoding(encoding)
+ read_ok = []
+ for filename in filenames:
+ try:
+ with open(filename, encoding=encoding) as fp:
+ self._read(fp, filename)
+ except OSError:
+ continue
+ if isinstance(filename, os.PathLike):
+ filename = os.fspath(filename)
+ read_ok.append(filename)
+ return read_ok
+
+ def read_file(self, f, source=None):
+ """Like read() but the argument must be a file-like object.
+
+ The `f' argument must be iterable, returning one line at a time.
+ Optional second argument is the `source' specifying the name of the
+ file being read. If not given, it is taken from f.name. If `f' has no
+ `name' attribute, `??>' is used.
+ """
+ if source is None:
+ try:
+ source = f.name
+ except AttributeError:
+ source = '??>'
+ self._read(f, source)
+
+ def read_string(self, string, source=''):
+ """Read configuration from a given string."""
+ sfile = io.StringIO(string)
+ self.read_file(sfile, source)
+
+ def read_dict(self, dictionary, source=''):
+ """Read configuration from a dictionary.
+
+ Keys are section names, values are dictionaries with keys and values
+ that should be present in the section. If the used dictionary type
+ preserves order, sections and their keys will be added in order.
+
+ All types held in the dictionary are converted to strings during
+ reading, including section names, option names and keys.
+
+ Optional second argument is the `source' specifying the name of the
+ dictionary being read.
+ """
+ elements_added = set()
+ for section, keys in dictionary.items():
+ section = str(section)
+ try:
+ self.add_section(section)
+ except (DuplicateSectionError, ValueError):
+ if self._strict and section in elements_added:
+ raise
+ elements_added.add(section)
+ for key, value in keys.items():
+ key = self.optionxform(str(key))
+ if value is not None:
+ value = str(value)
+ if self._strict and (section, key) in elements_added:
+ raise DuplicateOptionError(section, key, source)
+ elements_added.add((section, key))
+ self.set(section, key, value)
+
+ def readfp(self, fp, filename=None):
+ """Deprecated, use read_file instead."""
+ warnings.warn(
+ "This method will be removed in future versions. "
+ "Use 'parser.read_file()' instead.",
+ DeprecationWarning,
+ stacklevel=2,
+ )
+ self.read_file(fp, source=filename)
+
+ def get(self, section, option, *, raw=False, vars=None, fallback=_UNSET):
+ """Get an option value for a given section.
+
+ If `vars' is provided, it must be a dictionary. The option is looked up
+ in `vars' (if provided), `section', and in `DEFAULTSECT' in that order.
+ If the key is not found and `fallback' is provided, it is used as
+ a fallback value. `None' can be provided as a `fallback' value.
+
+ If interpolation is enabled and the optional argument `raw' is False,
+ all interpolations are expanded in the return values.
+
+ Arguments `raw', `vars', and `fallback' are keyword only.
+
+ The section DEFAULT is special.
+ """
+ try:
+ d = self._unify_values(section, vars)
+ except NoSectionError:
+ if fallback is _UNSET:
+ raise
+ else:
+ return fallback
+ option = self.optionxform(option)
+ try:
+ value = d[option]
+ except KeyError:
+ if fallback is _UNSET:
+ raise NoOptionError(option, section)
+ else:
+ return fallback
+
+ if raw or value is None:
+ return value
+ else:
+ return self._interpolation.before_get(self, section, option, value, d)
+
+ def _get(self, section, conv, option, **kwargs):
+ return conv(self.get(section, option, **kwargs))
+
+ def _get_conv(
+ self, section, option, conv, *, raw=False, vars=None, fallback=_UNSET, **kwargs
+ ):
+ try:
+ return self._get(section, conv, option, raw=raw, vars=vars, **kwargs)
+ except (NoSectionError, NoOptionError):
+ if fallback is _UNSET:
+ raise
+ return fallback
+
+ # getint, getfloat and getboolean provided directly for backwards compat
+ def getint(
+ self, section, option, *, raw=False, vars=None, fallback=_UNSET, **kwargs
+ ):
+ return self._get_conv(
+ section, option, int, raw=raw, vars=vars, fallback=fallback, **kwargs
+ )
+
+ def getfloat(
+ self, section, option, *, raw=False, vars=None, fallback=_UNSET, **kwargs
+ ):
+ return self._get_conv(
+ section, option, float, raw=raw, vars=vars, fallback=fallback, **kwargs
+ )
+
+ def getboolean(
+ self, section, option, *, raw=False, vars=None, fallback=_UNSET, **kwargs
+ ):
+ return self._get_conv(
+ section,
+ option,
+ self._convert_to_boolean,
+ raw=raw,
+ vars=vars,
+ fallback=fallback,
+ **kwargs,
+ )
+
+ def items(self, section=_UNSET, raw=False, vars=None):
+ """Return a list of (name, value) tuples for each option in a section.
+
+ All % interpolations are expanded in the return values, based on the
+ defaults passed into the constructor, unless the optional argument
+ `raw' is true. Additional substitutions may be provided using the
+ `vars' argument, which must be a dictionary whose contents overrides
+ any pre-existing defaults.
+
+ The section DEFAULT is special.
+ """
+ if section is _UNSET:
+ return super(RawConfigParser, self).items()
+ d = self._defaults.copy()
+ try:
+ d.update(self._sections[section])
+ except KeyError:
+ if section != self.default_section:
+ raise NoSectionError(section)
+ orig_keys = list(d.keys())
+ # Update with the entry specific variables
+ if vars:
+ for key, value in vars.items():
+ d[self.optionxform(key)] = value
+
+ def value_getter_interp(option):
+ return self._interpolation.before_get(self, section, option, d[option], d)
+
+ def value_getter_raw(option):
+ return d[option]
+
+ value_getter = value_getter_raw if raw else value_getter_interp
+
+ return [(option, value_getter(option)) for option in orig_keys]
+
+ def popitem(self):
+ """Remove a section from the parser and return it as
+ a (section_name, section_proxy) tuple. If no section is present, raise
+ KeyError.
+
+ The section DEFAULT is never returned because it cannot be removed.
+ """
+ for key in self.sections():
+ value = self[key]
+ del self[key]
+ return key, value
+ raise KeyError
+
+ def optionxform(self, optionstr):
+ return optionstr.lower()
+
+ def has_option(self, section, option):
+ """Check for the existence of a given option in a given section.
+ If the specified `section' is None or an empty string, DEFAULT is
+ assumed. If the specified `section' does not exist, returns False."""
+ if not section or section == self.default_section:
+ option = self.optionxform(option)
+ return option in self._defaults
+ elif section not in self._sections:
+ return False
+ else:
+ option = self.optionxform(option)
+ return option in self._sections[section] or option in self._defaults
+
+ def set(self, section, option, value=None):
+ """Set an option."""
+ if value:
+ value = self._interpolation.before_set(self, section, option, value)
+ if not section or section == self.default_section:
+ sectdict = self._defaults
+ else:
+ try:
+ sectdict = self._sections[section]
+ except KeyError:
+ raise NoSectionError(section) from None
+ sectdict[self.optionxform(option)] = value
+
+ def write(self, fp, space_around_delimiters=True):
+ """Write an .ini-format representation of the configuration state.
+
+ If `space_around_delimiters' is True (the default), delimiters
+ between keys and values are surrounded by spaces.
+
+ Please note that comments in the original configuration file are not
+ preserved when writing the configuration back.
+ """
+ if space_around_delimiters:
+ d = " {} ".format(self._delimiters[0])
+ else:
+ d = self._delimiters[0]
+ if self._defaults:
+ self._write_section(fp, self.default_section, self._defaults.items(), d)
+ for section in self._sections:
+ self._write_section(fp, section, self._sections[section].items(), d)
+
+ def _write_section(self, fp, section_name, section_items, delimiter):
+ """Write a single section to the specified `fp'."""
+ fp.write("[{}]\n".format(section_name))
+ for key, value in section_items:
+ value = self._interpolation.before_write(self, section_name, key, value)
+ if value is not None or not self._allow_no_value:
+ value = delimiter + str(value).replace('\n', '\n\t')
+ else:
+ value = ""
+ fp.write("{}{}\n".format(key, value))
+ fp.write("\n")
+
+ def remove_option(self, section, option):
+ """Remove an option."""
+ if not section or section == self.default_section:
+ sectdict = self._defaults
+ else:
+ try:
+ sectdict = self._sections[section]
+ except KeyError:
+ raise NoSectionError(section) from None
+ option = self.optionxform(option)
+ existed = option in sectdict
+ if existed:
+ del sectdict[option]
+ return existed
+
+ def remove_section(self, section):
+ """Remove a file section."""
+ existed = section in self._sections
+ if existed:
+ del self._sections[section]
+ del self._proxies[section]
+ return existed
+
+ def __getitem__(self, key):
+ if key != self.default_section and not self.has_section(key):
+ raise KeyError(key)
+ return self._proxies[key]
+
+ def __setitem__(self, key, value):
+ # To conform with the mapping protocol, overwrites existing values in
+ # the section.
+ if key in self and self[key] is value:
+ return
+ # XXX this is not atomic if read_dict fails at any point. Then again,
+ # no update method in configparser is atomic in this implementation.
+ if key == self.default_section:
+ self._defaults.clear()
+ elif key in self._sections:
+ self._sections[key].clear()
+ self.read_dict({key: value})
+
+ def __delitem__(self, key):
+ if key == self.default_section:
+ raise ValueError("Cannot remove the default section.")
+ if not self.has_section(key):
+ raise KeyError(key)
+ self.remove_section(key)
+
+ def __contains__(self, key):
+ return key == self.default_section or self.has_section(key)
+
+ def __len__(self):
+ return len(self._sections) + 1 # the default section
+
+ def __iter__(self):
+ # XXX does it break when underlying container state changed?
+ return itertools.chain((self.default_section,), self._sections.keys())
+
+ def _read(self, fp, fpname): # noqa: C901
+ """Parse a sectioned configuration file.
+
+ Each section in a configuration file contains a header, indicated by
+ a name in square brackets (`[]'), plus key/value options, indicated by
+ `name' and `value' delimited with a specific substring (`=' or `:' by
+ default).
+
+ Values can span multiple lines, as long as they are indented deeper
+ than the first line of the value. Depending on the parser's mode, blank
+ lines may be treated as parts of multiline values or ignored.
+
+ Configuration files may include comments, prefixed by specific
+ characters (`#' and `;' by default). Comments may appear on their own
+ in an otherwise empty line or may be entered in lines holding values or
+ section names. Please note that comments get stripped off when reading
+ configuration files.
+ """
+ elements_added = set()
+ cursect = None # None, or a dictionary
+ sectname = None
+ optname = None
+ lineno = 0
+ indent_level = 0
+ e = None # None, or an exception
+ for lineno, line in enumerate(fp, start=1):
+ comment_start = sys.maxsize
+ # strip inline comments
+ inline_prefixes = {p: -1 for p in self._inline_comment_prefixes}
+ while comment_start == sys.maxsize and inline_prefixes:
+ next_prefixes = {}
+ for prefix, index in inline_prefixes.items():
+ index = line.find(prefix, index + 1)
+ if index == -1:
+ continue
+ next_prefixes[prefix] = index
+ if index == 0 or (index > 0 and line[index - 1].isspace()):
+ comment_start = min(comment_start, index)
+ inline_prefixes = next_prefixes
+ # strip full line comments
+ for prefix in self._comment_prefixes:
+ if line.strip().startswith(prefix):
+ comment_start = 0
+ break
+ if comment_start == sys.maxsize:
+ comment_start = None
+ value = line[:comment_start].strip()
+ if not value:
+ if self._empty_lines_in_values:
+ # add empty line to the value, but only if there was no
+ # comment on the line
+ if (
+ comment_start is None
+ and cursect is not None
+ and optname
+ and cursect[optname] is not None
+ ):
+ cursect[optname].append('') # newlines added at join
+ else:
+ # empty line marks end of value
+ indent_level = sys.maxsize
+ continue
+ # continuation line?
+ first_nonspace = self.NONSPACECRE.search(line)
+ cur_indent_level = first_nonspace.start() if first_nonspace else 0
+ if cursect is not None and optname and cur_indent_level > indent_level:
+ cursect[optname].append(value)
+ # a section header or option header?
+ else:
+ indent_level = cur_indent_level
+ # is it a section header?
+ mo = self.SECTCRE.match(value)
+ if mo:
+ sectname = mo.group('header')
+ if sectname in self._sections:
+ if self._strict and sectname in elements_added:
+ raise DuplicateSectionError(sectname, fpname, lineno)
+ cursect = self._sections[sectname]
+ elements_added.add(sectname)
+ elif sectname == self.default_section:
+ cursect = self._defaults
+ else:
+ cursect = self._dict()
+ self._sections[sectname] = cursect
+ self._proxies[sectname] = SectionProxy(self, sectname)
+ elements_added.add(sectname)
+ # So sections can't start with a continuation line
+ optname = None
+ # no section header in the file?
+ elif cursect is None:
+ raise MissingSectionHeaderError(fpname, lineno, line)
+ # an option line?
+ else:
+ mo = self._optcre.match(value)
+ if mo:
+ optname, vi, optval = mo.group('option', 'vi', 'value')
+ if not optname:
+ e = self._handle_error(e, fpname, lineno, line)
+ optname = self.optionxform(optname.rstrip())
+ if self._strict and (sectname, optname) in elements_added:
+ raise DuplicateOptionError(
+ sectname, optname, fpname, lineno
+ )
+ elements_added.add((sectname, optname))
+ # This check is fine because the OPTCRE cannot
+ # match if it would set optval to None
+ if optval is not None:
+ optval = optval.strip()
+ cursect[optname] = [optval]
+ else:
+ # valueless option handling
+ cursect[optname] = None
+ else:
+ # a non-fatal parsing error occurred. set up the
+ # exception but keep going. the exception will be
+ # raised at the end of the file and will contain a
+ # list of all bogus lines
+ e = self._handle_error(e, fpname, lineno, line)
+ self._join_multiline_values()
+ # if any parsing errors occurred, raise an exception
+ if e:
+ raise e
+
+ def _join_multiline_values(self):
+ defaults = self.default_section, self._defaults
+ all_sections = itertools.chain((defaults,), self._sections.items())
+ for section, options in all_sections:
+ for name, val in options.items():
+ if isinstance(val, list):
+ val = '\n'.join(val).rstrip()
+ options[name] = self._interpolation.before_read(
+ self, section, name, val
+ )
+
+ def _read_defaults(self, defaults):
+ """Read the defaults passed in the initializer.
+ Note: values can be non-string."""
+ for key, value in defaults.items():
+ self._defaults[self.optionxform(key)] = value
+
+ def _handle_error(self, exc, fpname, lineno, line):
+ if not exc:
+ exc = ParsingError(fpname)
+ exc.append(lineno, repr(line))
+ return exc
+
+ def _unify_values(self, section, vars):
+ """Create a sequence of lookups with 'vars' taking priority over
+ the 'section' which takes priority over the DEFAULTSECT.
+
+ """
+ sectiondict = {}
+ try:
+ sectiondict = self._sections[section]
+ except KeyError:
+ if section != self.default_section:
+ raise NoSectionError(section)
+ # Update with the entry specific variables
+ vardict = {}
+ if vars:
+ for key, value in vars.items():
+ if value is not None:
+ value = str(value)
+ vardict[self.optionxform(key)] = value
+ return _ChainMap(vardict, sectiondict, self._defaults)
+
+ def _convert_to_boolean(self, value):
+ """Return a boolean value translating from other types if necessary."""
+ if value.lower() not in self.BOOLEAN_STATES:
+ raise ValueError('Not a boolean: %s' % value)
+ return self.BOOLEAN_STATES[value.lower()]
+
+ def _validate_value_types(self, *, section="", option="", value=""):
+ """Raises a TypeError for non-string values.
+
+ The only legal non-string value if we allow valueless
+ options is None, so we need to check if the value is a
+ string if:
+ - we do not allow valueless options, or
+ - we allow valueless options but the value is not None
+
+ For compatibility reasons this method is not used in classic set()
+ for RawConfigParsers. It is invoked in every case for mapping protocol
+ access and in ConfigParser.set().
+ """
+ if not isinstance(section, str):
+ raise TypeError("section names must be strings")
+ if not isinstance(option, str):
+ raise TypeError("option keys must be strings")
+ if not self._allow_no_value or value:
+ if not isinstance(value, str):
+ raise TypeError("option values must be strings")
+
+ @property
+ def converters(self):
+ return self._converters
+
+
+class ConfigParser(RawConfigParser):
+ """ConfigParser implementing interpolation."""
+
+ _DEFAULT_INTERPOLATION = BasicInterpolation()
+
+ def set(self, section, option, value=None):
+ """Set an option. Extends RawConfigParser.set by validating type and
+ interpolation syntax on the value."""
+ self._validate_value_types(option=option, value=value)
+ super().set(section, option, value)
+
+ def add_section(self, section):
+ """Create a new section in the configuration. Extends
+ RawConfigParser.add_section by validating if the section name is
+ a string."""
+ self._validate_value_types(section=section)
+ super().add_section(section)
+
+ def _read_defaults(self, defaults):
+ """Reads the defaults passed in the initializer, implicitly converting
+ values to strings like the rest of the API.
+
+ Does not perform interpolation for backwards compatibility.
+ """
+ try:
+ hold_interpolation = self._interpolation
+ self._interpolation = Interpolation()
+ self.read_dict({self.default_section: defaults})
+ finally:
+ self._interpolation = hold_interpolation
+
+
+class SafeConfigParser(ConfigParser):
+ """ConfigParser alias for backwards compatibility purposes."""
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ warnings.warn(
+ "The SafeConfigParser class has been renamed to ConfigParser "
+ "in Python 3.2. This alias will be removed in future versions."
+ " Use ConfigParser directly instead.",
+ DeprecationWarning,
+ stacklevel=2,
+ )
+
+
+class SectionProxy(MutableMapping):
+ """A proxy for a single section from a parser."""
+
+ def __init__(self, parser, name):
+ """Creates a view on a section of the specified `name` in `parser`."""
+ self._parser = parser
+ self._name = name
+ for conv in parser.converters:
+ key = 'get' + conv
+ getter = functools.partial(self.get, _impl=getattr(parser, key))
+ setattr(self, key, getter)
+
+ def __repr__(self):
+ return ''.format(self._name)
+
+ def __getitem__(self, key):
+ if not self._parser.has_option(self._name, key):
+ raise KeyError(key)
+ return self._parser.get(self._name, key)
+
+ def __setitem__(self, key, value):
+ self._parser._validate_value_types(option=key, value=value)
+ return self._parser.set(self._name, key, value)
+
+ def __delitem__(self, key):
+ if not (
+ self._parser.has_option(self._name, key)
+ and self._parser.remove_option(self._name, key)
+ ):
+ raise KeyError(key)
+
+ def __contains__(self, key):
+ return self._parser.has_option(self._name, key)
+
+ def __len__(self):
+ return len(self._options())
+
+ def __iter__(self):
+ return self._options().__iter__()
+
+ def _options(self):
+ if self._name != self._parser.default_section:
+ return self._parser.options(self._name)
+ else:
+ return self._parser.defaults()
+
+ @property
+ def parser(self):
+ # The parser object of the proxy is read-only.
+ return self._parser
+
+ @property
+ def name(self):
+ # The name of the section on a proxy is read-only.
+ return self._name
+
+ def get(self, option, fallback=None, *, raw=False, vars=None, _impl=None, **kwargs):
+ """Get an option value.
+
+ Unless `fallback` is provided, `None` will be returned if the option
+ is not found.
+
+ """
+ # If `_impl` is provided, it should be a getter method on the parser
+ # object that provides the desired type conversion.
+ if not _impl:
+ _impl = self._parser.get
+ return _impl(
+ self._name, option, raw=raw, vars=vars, fallback=fallback, **kwargs
+ )
+
+
+class ConverterMapping(MutableMapping):
+ """Enables reuse of get*() methods between the parser and section proxies.
+
+ If a parser class implements a getter directly, the value for the given
+ key will be ``None``. The presence of the converter name here enables
+ section proxies to find and use the implementation on the parser class.
+ """
+
+ GETTERCRE = re.compile(r"^get(?P.+)$")
+
+ def __init__(self, parser):
+ self._parser = parser
+ self._data = {}
+ for getter in dir(self._parser):
+ m = self.GETTERCRE.match(getter)
+ if not m or not callable(getattr(self._parser, getter)):
+ continue
+ self._data[m.group('name')] = None # See class docstring.
+
+ def __getitem__(self, key):
+ return self._data[key]
+
+ def __setitem__(self, key, value):
+ try:
+ k = 'get' + key
+ except TypeError:
+ raise ValueError(
+ 'Incompatible key: {} (type: {})' ''.format(key, type(key))
+ )
+ if k == 'get':
+ raise ValueError('Incompatible key: cannot use "" as a name')
+ self._data[key] = value
+ func = functools.partial(self._parser._get_conv, conv=value)
+ func.converter = value
+ setattr(self._parser, k, func)
+ for proxy in self._parser.values():
+ getter = functools.partial(proxy.get, _impl=func)
+ setattr(proxy, k, getter)
+
+ def __delitem__(self, key):
+ try:
+ k = 'get' + (key or None)
+ except TypeError:
+ raise KeyError(key)
+ del self._data[key]
+ for inst in itertools.chain((self._parser,), self._parser.values()):
+ try:
+ delattr(inst, k)
+ except AttributeError:
+ # don't raise since the entry was present in _data, silently
+ # clean up
+ continue
+
+ def __iter__(self):
+ return iter(self._data)
+
+ def __len__(self):
+ return len(self._data)
diff --git a/venv/Lib/site-packages/backports/configparser/compat.py b/venv/Lib/site-packages/backports/configparser/compat.py
new file mode 100644
index 0000000..951bfea
--- /dev/null
+++ b/venv/Lib/site-packages/backports/configparser/compat.py
@@ -0,0 +1,19 @@
+import types
+import io as _io
+
+
+def text_encoding(encoding, stacklevel=2):
+ """
+ Stubbed version of io.text_encoding as found in Python 3.10
+ """
+ return encoding
+
+
+def copy_module(mod, **defaults):
+ copy = types.ModuleType(mod.__name__, doc=mod.__doc__)
+ vars(copy).update(defaults)
+ vars(copy).update(vars(mod))
+ return copy
+
+
+io = copy_module(_io, text_encoding=text_encoding)
diff --git a/venv/Lib/site-packages/configparser-5.2.0.dist-info/INSTALLER b/venv/Lib/site-packages/configparser-5.2.0.dist-info/INSTALLER
new file mode 100644
index 0000000..a1b589e
--- /dev/null
+++ b/venv/Lib/site-packages/configparser-5.2.0.dist-info/INSTALLER
@@ -0,0 +1 @@
+pip
diff --git a/venv/Lib/site-packages/configparser-5.2.0.dist-info/LICENSE b/venv/Lib/site-packages/configparser-5.2.0.dist-info/LICENSE
new file mode 100644
index 0000000..353924b
--- /dev/null
+++ b/venv/Lib/site-packages/configparser-5.2.0.dist-info/LICENSE
@@ -0,0 +1,19 @@
+Copyright Jason R. Coombs
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to
+deal in the Software without restriction, including without limitation the
+rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+sell copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+IN THE SOFTWARE.
diff --git a/venv/Lib/site-packages/configparser-5.2.0.dist-info/METADATA b/venv/Lib/site-packages/configparser-5.2.0.dist-info/METADATA
new file mode 100644
index 0000000..fe728ae
--- /dev/null
+++ b/venv/Lib/site-packages/configparser-5.2.0.dist-info/METADATA
@@ -0,0 +1,267 @@
+Metadata-Version: 2.1
+Name: configparser
+Version: 5.2.0
+Summary: Updated configparser from Python 3.8 for Python 2.6+.
+Home-page: https://github.com/jaraco/configparser/
+Author: Łukasz Langa
+Author-email: lukasz@langa.pl
+Maintainer: Jason R. Coombs
+Maintainer-email: jaraco@jaraco.com
+License: UNKNOWN
+Keywords: configparser ini parsing conf cfg configuration file
+Platform: any
+Classifier: Development Status :: 5 - Production/Stable
+Classifier: Intended Audience :: Developers
+Classifier: License :: OSI Approved :: MIT License
+Classifier: Programming Language :: Python :: 3
+Classifier: Programming Language :: Python :: 3 :: Only
+Requires-Python: >=3.6
+License-File: LICENSE
+Provides-Extra: docs
+Requires-Dist: sphinx ; extra == 'docs'
+Requires-Dist: jaraco.packaging (>=8.2) ; extra == 'docs'
+Requires-Dist: rst.linker (>=1.9) ; extra == 'docs'
+Requires-Dist: jaraco.tidelift (>=1.4) ; extra == 'docs'
+Provides-Extra: testing
+Requires-Dist: pytest (>=6) ; extra == 'testing'
+Requires-Dist: pytest-checkdocs (>=2.4) ; extra == 'testing'
+Requires-Dist: pytest-flake8 ; extra == 'testing'
+Requires-Dist: pytest-cov ; extra == 'testing'
+Requires-Dist: pytest-enabler (>=1.0.1) ; extra == 'testing'
+Requires-Dist: types-backports ; extra == 'testing'
+Requires-Dist: pytest-black (>=0.3.7) ; (platform_python_implementation != "PyPy") and extra == 'testing'
+Requires-Dist: pytest-mypy ; (platform_python_implementation != "PyPy") and extra == 'testing'
+
+.. image:: https://img.shields.io/pypi/v/configparser.svg
+ :target: `PyPI link`_
+
+.. image:: https://img.shields.io/pypi/pyversions/configparser.svg
+ :target: `PyPI link`_
+
+.. _PyPI link: https://pypi.org/project/configparser
+
+.. image:: https://github.com/jaraco/configparser/workflows/tests/badge.svg
+ :target: https://github.com/jaraco/configparser/actions?query=workflow%3A%22tests%22
+ :alt: tests
+
+.. image:: https://img.shields.io/badge/code%20style-black-000000.svg
+ :target: https://github.com/psf/black
+ :alt: Code style: Black
+
+.. image:: https://readthedocs.org/projects/configparser/badge/?version=latest
+ :target: https://configparser.readthedocs.io/en/latest/?badge=latest
+
+.. image:: https://img.shields.io/badge/skeleton-2021-informational
+ :target: https://blog.jaraco.com/skeleton
+
+.. image:: https://tidelift.com/badges/package/pypi/configparser
+ :target: https://tidelift.com/subscription/pkg/pypi-configparser?utm_source=pypi-configparser&utm_medium=readme
+
+
+This package is a backport of the refreshed and enhanced ConfigParser from
+later Python versions. To use the backport instead of the built-in version,
+simply import it explicitly as a backport::
+
+ from backports import configparser
+
+To use the backport on Python 2 and the built-in version on
+Python 3, use the standard invocation::
+
+ import configparser
+
+For detailed documentation consult the vanilla version at
+http://docs.python.org/3/library/configparser.html.
+
+Why you'll love ``configparser``
+--------------------------------
+
+Whereas almost completely compatible with its older brother, ``configparser``
+sports a bunch of interesting new features:
+
+* full mapping protocol access (`more info
+ `_)::
+
+ >>> parser = ConfigParser()
+ >>> parser.read_string("""
+ [DEFAULT]
+ location = upper left
+ visible = yes
+ editable = no
+ color = blue
+
+ [main]
+ title = Main Menu
+ color = green
+
+ [options]
+ title = Options
+ """)
+ >>> parser['main']['color']
+ 'green'
+ >>> parser['main']['editable']
+ 'no'
+ >>> section = parser['options']
+ >>> section['title']
+ 'Options'
+ >>> section['title'] = 'Options (editable: %(editable)s)'
+ >>> section['title']
+ 'Options (editable: no)'
+
+* there's now one default ``ConfigParser`` class, which basically is the old
+ ``SafeConfigParser`` with a bunch of tweaks which make it more predictable for
+ users. Don't need interpolation? Simply use
+ ``ConfigParser(interpolation=None)``, no need to use a distinct
+ ``RawConfigParser`` anymore.
+
+* the parser is highly `customizable upon instantiation
+ `__
+ supporting things like changing option delimiters, comment characters, the
+ name of the DEFAULT section, the interpolation syntax, etc.
+
+* you can easily create your own interpolation syntax but there are two powerful
+ implementations built-in (`more info
+ `__):
+
+ * the classic ``%(string-like)s`` syntax (called ``BasicInterpolation``)
+
+ * a new ``${buildout:like}`` syntax (called ``ExtendedInterpolation``)
+
+* fallback values may be specified in getters (`more info
+ `__)::
+
+ >>> config.get('closet', 'monster',
+ ... fallback='No such things as monsters')
+ 'No such things as monsters'
+
+* ``ConfigParser`` objects can now read data directly `from strings
+ `__
+ and `from dictionaries
+ `__.
+ That means importing configuration from JSON or specifying default values for
+ the whole configuration (multiple sections) is now a single line of code. Same
+ goes for copying data from another ``ConfigParser`` instance, thanks to its
+ mapping protocol support.
+
+* many smaller tweaks, updates and fixes
+
+A few words about Unicode
+-------------------------
+
+``configparser`` comes from Python 3 and as such it works well with Unicode.
+The library is generally cleaned up in terms of internal data storage and
+reading/writing files. There are a couple of incompatibilities with the old
+``ConfigParser`` due to that. However, the work required to migrate is well
+worth it as it shows the issues that would likely come up during migration of
+your project to Python 3.
+
+The design assumes that Unicode strings are used whenever possible [1]_. That
+gives you the certainty that what's stored in a configuration object is text.
+Once your configuration is read, the rest of your application doesn't have to
+deal with encoding issues. All you have is text [2]_. The only two phases when
+you should explicitly state encoding is when you either read from an external
+source (e.g. a file) or write back.
+
+Versioning
+----------
+
+This project uses `semver `_ to
+communicate the impact of various releases while periodically syncing
+with the upstream implementation in CPython.
+The `history `_
+serves as a reference indicating which versions incorporate
+which upstream functionality.
+
+Prior to the ``4.0.0`` release, `another scheme
+`_
+was used to associate the CPython and backports releases.
+
+Maintenance
+-----------
+
+This backport was originally authored by Łukasz Langa, the current vanilla
+``configparser`` maintainer for CPython and is currently maintained by
+Jason R. Coombs:
+
+* `configparser repository `_
+
+* `configparser issue tracker `_
+
+For Enterprise
+==============
+
+Available as part of the Tidelift Subscription.
+
+This project and the maintainers of thousands of other packages are working with Tidelift to deliver one enterprise subscription that covers all of the open source you use.
+
+`Learn more `_.
+
+Security Contact
+----------------
+
+To report a security vulnerability, please use the
+`Tidelift security contact `_.
+Tidelift will coordinate the fix and disclosure.
+
+Conversion Process
+------------------
+
+This section is technical and should bother you only if you are wondering how
+this backport is produced. If the implementation details of this backport are
+not important for you, feel free to ignore the following content.
+
+The project takes the following branching approach:
+
+* The ``3.x`` branch holds unchanged files synchronized from the upstream
+ CPython repository. The synchronization is currently done by manually copying
+ the required files and stating from which CPython changeset they come.
+
+* The ``main`` branch holds a version of the ``3.x`` code with some tweaks
+ that make it compatible with older Pythons. Code on this branch must work
+ on all supported Python versions. Test with ``tox`` or in CI.
+
+The process works like this:
+
+1. In the ``3.x`` branch, run ``pip-run -- sync-upstream.py``, which
+ downloads the latest stable release of Python and copies the relevant
+ files from there into their new locations and then commits those
+ changes with a nice reference to the relevant upstream commit hash.
+
+2. Check for new names in ``__all__`` and update imports in
+ ``configparser.py`` accordingly. Optionally, run the tests on a late
+ Python 3. Commit.
+
+3. Merge the new commit to ``main``. Run tests. Commit.
+
+4. Make any compatibility changes on ``main``. Run tests. Commit.
+
+5. Update the docs and release the new version.
+
+
+Footnotes
+---------
+
+.. [1] To somewhat ease migration, passing bytestrings is still supported but
+ they are converted to Unicode for internal storage anyway. This means
+ that for the vast majority of strings used in configuration files, it
+ won't matter if you pass them as bytestrings or Unicode. However, if you
+ pass a bytestring that cannot be converted to Unicode using the naive
+ ASCII codec, a ``UnicodeDecodeError`` will be raised. This is purposeful
+ and helps you manage proper encoding for all content you store in
+ memory, read from various sources and write back.
+
+.. [2] Life gets much easier when you understand that you basically manage
+ **text** in your application. You don't care about bytes but about
+ letters. In that regard the concept of content encoding is meaningless.
+ The only time when you deal with raw bytes is when you write the data to
+ a file. Then you have to specify how your text should be encoded. On
+ the other end, to get meaningful text from a file, the application
+ reading it has to know which encoding was used during its creation. But
+ once the bytes are read and properly decoded, all you have is text. This
+ is especially powerful when you start interacting with multiple data
+ sources. Even if each of them uses a different encoding, inside your
+ application data is held in abstract text form. You can program your
+ business logic without worrying about which data came from which source.
+ You can freely exchange the data you store between sources. Only
+ reading/writing files requires encoding your text to bytes.
+
+
diff --git a/venv/Lib/site-packages/configparser-5.2.0.dist-info/RECORD b/venv/Lib/site-packages/configparser-5.2.0.dist-info/RECORD
new file mode 100644
index 0000000..87b0ea0
--- /dev/null
+++ b/venv/Lib/site-packages/configparser-5.2.0.dist-info/RECORD
@@ -0,0 +1,13 @@
+__pycache__/configparser.cpython-39.pyc,,
+backports/configparser/__init__.py,sha256=zjgAe9lH7_cTCyt7e-iyfyi007PfaMAVPhJCjM_TUPs,54655
+backports/configparser/__pycache__/__init__.cpython-39.pyc,,
+backports/configparser/__pycache__/compat.cpython-39.pyc,,
+backports/configparser/compat.py,sha256=Z3Fo6AI4BfFRfNd9Fj8n1Yw2NW2r112uQTFEnF659Lc,404
+configparser-5.2.0.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
+configparser-5.2.0.dist-info/LICENSE,sha256=2z8CRrH5J48VhFuZ_sR4uLUG63ZIeZNyL4xuJUKF-vg,1050
+configparser-5.2.0.dist-info/METADATA,sha256=4MeDipErpJIH61Zwu6BQ9f951Qu5HRg5RKFu3LYNZck,11053
+configparser-5.2.0.dist-info/RECORD,,
+configparser-5.2.0.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
+configparser-5.2.0.dist-info/WHEEL,sha256=ewwEueio1C2XeHTvT17n8dZUJgOvyCWCt0WVNLClP9o,92
+configparser-5.2.0.dist-info/top_level.txt,sha256=mIs8gajd7cvEWhVluv4u6ocaHw_TJ9rOrpkZEFv-7Hc,23
+configparser.py,sha256=4VADEswCwzy_RDVgvje3BmZhD6iwo3k4EkUZcgzLD4M,1546
diff --git a/venv/Lib/site-packages/configparser-5.2.0.dist-info/REQUESTED b/venv/Lib/site-packages/configparser-5.2.0.dist-info/REQUESTED
new file mode 100644
index 0000000..e69de29
diff --git a/venv/Lib/site-packages/configparser-5.2.0.dist-info/WHEEL b/venv/Lib/site-packages/configparser-5.2.0.dist-info/WHEEL
new file mode 100644
index 0000000..5bad85f
--- /dev/null
+++ b/venv/Lib/site-packages/configparser-5.2.0.dist-info/WHEEL
@@ -0,0 +1,5 @@
+Wheel-Version: 1.0
+Generator: bdist_wheel (0.37.0)
+Root-Is-Purelib: true
+Tag: py3-none-any
+
diff --git a/venv/Lib/site-packages/configparser-5.2.0.dist-info/top_level.txt b/venv/Lib/site-packages/configparser-5.2.0.dist-info/top_level.txt
new file mode 100644
index 0000000..a6cb03a
--- /dev/null
+++ b/venv/Lib/site-packages/configparser-5.2.0.dist-info/top_level.txt
@@ -0,0 +1,2 @@
+backports
+configparser
diff --git a/venv/Lib/site-packages/configparser.py b/venv/Lib/site-packages/configparser.py
new file mode 100644
index 0000000..0a18360
--- /dev/null
+++ b/venv/Lib/site-packages/configparser.py
@@ -0,0 +1,61 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+"""Convenience module importing everything from backports.configparser."""
+
+from __future__ import absolute_import
+from __future__ import division
+from __future__ import print_function
+from __future__ import unicode_literals
+
+from backports.configparser import (
+ RawConfigParser,
+ ConfigParser,
+ SafeConfigParser,
+ SectionProxy,
+ Interpolation,
+ BasicInterpolation,
+ ExtendedInterpolation,
+ LegacyInterpolation,
+ NoSectionError,
+ DuplicateSectionError,
+ DuplicateOptionError,
+ NoOptionError,
+ InterpolationError,
+ InterpolationMissingOptionError,
+ InterpolationSyntaxError,
+ InterpolationDepthError,
+ ParsingError,
+ MissingSectionHeaderError,
+ ConverterMapping,
+ DEFAULTSECT,
+ MAX_INTERPOLATION_DEPTH,
+)
+
+from backports.configparser import Error, _UNSET, _default_dict, _ChainMap # noqa: F401
+
+__all__ = [
+ "NoSectionError",
+ "DuplicateOptionError",
+ "DuplicateSectionError",
+ "NoOptionError",
+ "InterpolationError",
+ "InterpolationDepthError",
+ "InterpolationMissingOptionError",
+ "InterpolationSyntaxError",
+ "ParsingError",
+ "MissingSectionHeaderError",
+ "ConfigParser",
+ "SafeConfigParser",
+ "RawConfigParser",
+ "Interpolation",
+ "BasicInterpolation",
+ "ExtendedInterpolation",
+ "LegacyInterpolation",
+ "SectionProxy",
+ "ConverterMapping",
+ "DEFAULTSECT",
+ "MAX_INTERPOLATION_DEPTH",
+]
+
+# NOTE: names missing from __all__ imported anyway for backwards compatibility.
diff --git a/venv/Lib/site-packages/django/bin/django-admin.py b/venv/Lib/site-packages/django/bin/django-admin.py
new file mode 100644
index 0000000..594b0f1
--- /dev/null
+++ b/venv/Lib/site-packages/django/bin/django-admin.py
@@ -0,0 +1,21 @@
+#!/usr/bin/env python
+# When the django-admin.py deprecation ends, remove this script.
+import warnings
+
+from django.core import management
+
+try:
+ from django.utils.deprecation import RemovedInDjango40Warning
+except ImportError:
+ raise ImportError(
+ 'django-admin.py was deprecated in Django 3.1 and removed in Django '
+ '4.0. Please manually remove this script from your virtual environment '
+ 'and use django-admin instead.'
+ )
+
+if __name__ == "__main__":
+ warnings.warn(
+ 'django-admin.py is deprecated in favor of django-admin.',
+ RemovedInDjango40Warning,
+ )
+ management.execute_from_command_line()
diff --git a/venv/Lib/site-packages/django/contrib/postgres/forms/jsonb.py b/venv/Lib/site-packages/django/contrib/postgres/forms/jsonb.py
new file mode 100644
index 0000000..ebc85ef
--- /dev/null
+++ b/venv/Lib/site-packages/django/contrib/postgres/forms/jsonb.py
@@ -0,0 +1,16 @@
+import warnings
+
+from django.forms import JSONField as BuiltinJSONField
+from django.utils.deprecation import RemovedInDjango40Warning
+
+__all__ = ['JSONField']
+
+
+class JSONField(BuiltinJSONField):
+ def __init__(self, *args, **kwargs):
+ warnings.warn(
+ 'django.contrib.postgres.forms.JSONField is deprecated in favor '
+ 'of django.forms.JSONField.',
+ RemovedInDjango40Warning, stacklevel=2,
+ )
+ super().__init__(*args, **kwargs)
diff --git a/venv/Lib/site-packages/django/db/migrations/operations/utils.py b/venv/Lib/site-packages/django/db/migrations/operations/utils.py
new file mode 100644
index 0000000..facfd9f
--- /dev/null
+++ b/venv/Lib/site-packages/django/db/migrations/operations/utils.py
@@ -0,0 +1,102 @@
+from collections import namedtuple
+
+from django.db.models.fields.related import RECURSIVE_RELATIONSHIP_CONSTANT
+
+
+def resolve_relation(model, app_label=None, model_name=None):
+ """
+ Turn a model class or model reference string and return a model tuple.
+
+ app_label and model_name are used to resolve the scope of recursive and
+ unscoped model relationship.
+ """
+ if isinstance(model, str):
+ if model == RECURSIVE_RELATIONSHIP_CONSTANT:
+ if app_label is None or model_name is None:
+ raise TypeError(
+ 'app_label and model_name must be provided to resolve '
+ 'recursive relationships.'
+ )
+ return app_label, model_name
+ if '.' in model:
+ app_label, model_name = model.split('.', 1)
+ return app_label, model_name.lower()
+ if app_label is None:
+ raise TypeError(
+ 'app_label must be provided to resolve unscoped model '
+ 'relationships.'
+ )
+ return app_label, model.lower()
+ return model._meta.app_label, model._meta.model_name
+
+
+FieldReference = namedtuple('FieldReference', 'to through')
+
+
+def field_references(
+ model_tuple,
+ field,
+ reference_model_tuple,
+ reference_field_name=None,
+ reference_field=None,
+):
+ """
+ Return either False or a FieldReference if `field` references provided
+ context.
+
+ False positives can be returned if `reference_field_name` is provided
+ without `reference_field` because of the introspection limitation it
+ incurs. This should not be an issue when this function is used to determine
+ whether or not an optimization can take place.
+ """
+ remote_field = field.remote_field
+ if not remote_field:
+ return False
+ references_to = None
+ references_through = None
+ if resolve_relation(remote_field.model, *model_tuple) == reference_model_tuple:
+ to_fields = getattr(field, 'to_fields', None)
+ if (
+ reference_field_name is None or
+ # Unspecified to_field(s).
+ to_fields is None or
+ # Reference to primary key.
+ (None in to_fields and (reference_field is None or reference_field.primary_key)) or
+ # Reference to field.
+ reference_field_name in to_fields
+ ):
+ references_to = (remote_field, to_fields)
+ through = getattr(remote_field, 'through', None)
+ if through and resolve_relation(through, *model_tuple) == reference_model_tuple:
+ through_fields = remote_field.through_fields
+ if (
+ reference_field_name is None or
+ # Unspecified through_fields.
+ through_fields is None or
+ # Reference to field.
+ reference_field_name in through_fields
+ ):
+ references_through = (remote_field, through_fields)
+ if not (references_to or references_through):
+ return False
+ return FieldReference(references_to, references_through)
+
+
+def get_references(state, model_tuple, field_tuple=()):
+ """
+ Generator of (model_state, name, field, reference) referencing
+ provided context.
+
+ If field_tuple is provided only references to this particular field of
+ model_tuple will be generated.
+ """
+ for state_model_tuple, model_state in state.models.items():
+ for name, field in model_state.fields.items():
+ reference = field_references(state_model_tuple, field, model_tuple, *field_tuple)
+ if reference:
+ yield model_state, name, field, reference
+
+
+def field_is_referenced(state, model_tuple, field_tuple):
+ """Return whether `field_tuple` is referenced by any state models."""
+ return next(get_references(state, model_tuple, field_tuple), None) is not None
diff --git a/venv/Lib/site-packages/django_filter-21.1.dist-info/INSTALLER b/venv/Lib/site-packages/django_filter-21.1.dist-info/INSTALLER
new file mode 100644
index 0000000..a1b589e
--- /dev/null
+++ b/venv/Lib/site-packages/django_filter-21.1.dist-info/INSTALLER
@@ -0,0 +1 @@
+pip
diff --git a/venv/Lib/site-packages/django_filter-21.1.dist-info/LICENSE b/venv/Lib/site-packages/django_filter-21.1.dist-info/LICENSE
new file mode 100644
index 0000000..4b73093
--- /dev/null
+++ b/venv/Lib/site-packages/django_filter-21.1.dist-info/LICENSE
@@ -0,0 +1,24 @@
+Copyright (c) Alex Gaynor and individual contributors.
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+ * Redistributions of source code must retain the above copyright notice, this
+ list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
+ and/or other materials provided with the distribution.
+ * The names of its contributors may not be used to endorse or promote products
+ derived from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
+ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/venv/Lib/site-packages/django_filter-21.1.dist-info/METADATA b/venv/Lib/site-packages/django_filter-21.1.dist-info/METADATA
new file mode 100644
index 0000000..8f1b555
--- /dev/null
+++ b/venv/Lib/site-packages/django_filter-21.1.dist-info/METADATA
@@ -0,0 +1,156 @@
+Metadata-Version: 2.1
+Name: django-filter
+Version: 21.1
+Summary: Django-filter is a reusable Django application for allowing users to filter querysets dynamically.
+Home-page: https://github.com/carltongibson/django-filter/tree/main
+Author: Alex Gaynor
+Author-email: alex.gaynor@gmail.com
+Maintainer: Carlton Gibson
+Maintainer-email: carlton.gibson@noumenal.es
+License: BSD
+Project-URL: Documentation, https://django-filter.readthedocs.io/en/main/
+Project-URL: Changelog, https://github.com/carltongibson/django-filter/blob/main/CHANGES.rst
+Project-URL: Bug Tracker, https://github.com/carltongibson/django-filter/issues
+Project-URL: Source Code, https://github.com/carltongibson/django-filter
+Platform: UNKNOWN
+Classifier: Development Status :: 5 - Production/Stable
+Classifier: Environment :: Web Environment
+Classifier: Intended Audience :: Developers
+Classifier: License :: OSI Approved :: BSD License
+Classifier: Operating System :: OS Independent
+Classifier: Framework :: Django
+Classifier: Framework :: Django :: 2.2
+Classifier: Framework :: Django :: 3.1
+Classifier: Framework :: Django :: 3.2
+Classifier: Framework :: Django :: 4.0
+Classifier: Programming Language :: Python
+Classifier: Programming Language :: Python :: 3
+Classifier: Programming Language :: Python :: 3.6
+Classifier: Programming Language :: Python :: 3.7
+Classifier: Programming Language :: Python :: 3.8
+Classifier: Programming Language :: Python :: 3.9
+Requires-Python: >=3.6
+License-File: LICENSE
+Requires-Dist: Django (>=2.2)
+
+Django Filter
+=============
+
+Django-filter is a reusable Django application allowing users to declaratively
+add dynamic ``QuerySet`` filtering from URL parameters.
+
+Full documentation on `read the docs`_.
+
+.. image:: https://codecov.io/gh/carltongibson/django-filter/branch/develop/graph/badge.svg
+ :target: https://codecov.io/gh/carltongibson/django-filter
+
+.. image:: https://badge.fury.io/py/django-filter.svg
+ :target: http://badge.fury.io/py/django-filter
+
+
+Versioning and stability policy
+-------------------------------
+
+Django-Filter is a mature and stable package. It uses a two-part CalVer
+versioning scheme, such as ``21.1``. The first number is the year. The second
+is the release number within that year.
+
+On an on-going basis, Django-Filter aims to support all current Django
+versions, the matching current Python versions, and the latest version of
+Django REST Framework.
+
+Please see:
+
+* `Status of supported Python branches `_
+* `List of supported Django versions `_
+
+Support for Python and Django versions will be dropped when they reach
+end-of-life. Support for Python versions will dropped when they reach
+end-of-life, even when still supported by a current version of Django.
+
+Other breaking changes are rare. Where required, every effort will be made to
+apply a "Year plus two" deprecation period. For example, a change initially
+introduced in ``23.x`` would offer a fallback where feasible and finally be
+removed in ``25.1``. Where fallbacks are not feasible, breaking changes without
+deprecation will be called out in the release notes.
+
+
+Installation
+------------
+
+Install using pip:
+
+.. code-block:: sh
+
+ pip install django-filter
+
+Then add ``'django_filters'`` to your ``INSTALLED_APPS``.
+
+.. code-block:: python
+
+ INSTALLED_APPS = [
+ ...
+ 'django_filters',
+ ]
+
+
+Usage
+-----
+
+Django-filter can be used for generating interfaces similar to the Django
+admin's ``list_filter`` interface. It has an API very similar to Django's
+``ModelForms``. For example, if you had a Product model you could have a
+filterset for it with the code:
+
+.. code-block:: python
+
+ import django_filters
+
+ class ProductFilter(django_filters.FilterSet):
+ class Meta:
+ model = Product
+ fields = ['name', 'price', 'manufacturer']
+
+
+And then in your view you could do:
+
+.. code-block:: python
+
+ def product_list(request):
+ filter = ProductFilter(request.GET, queryset=Product.objects.all())
+ return render(request, 'my_app/template.html', {'filter': filter})
+
+
+Usage with Django REST Framework
+--------------------------------
+
+Django-filter provides a custom ``FilterSet`` and filter backend for use with
+Django REST Framework.
+
+To use this adjust your import to use
+``django_filters.rest_framework.FilterSet``.
+
+.. code-block:: python
+
+ from django_filters import rest_framework as filters
+
+ class ProductFilter(filters.FilterSet):
+ class Meta:
+ model = Product
+ fields = ('category', 'in_stock')
+
+
+For more details see the `DRF integration docs`_.
+
+
+Support
+-------
+
+If you need help you can start a `discussion`_. For commercial support, please
+`contact Carlton Gibson via his website `_.
+
+.. _`discussion`: https://github.com/carltongibson/django-filter/discussions
+.. _`read the docs`: https://django-filter.readthedocs.io/en/main/
+.. _`DRF integration docs`: https://django-filter.readthedocs.io/en/stable/guide/rest_framework.html
+
+
diff --git a/venv/Lib/site-packages/django_filter-21.1.dist-info/RECORD b/venv/Lib/site-packages/django_filter-21.1.dist-info/RECORD
new file mode 100644
index 0000000..f738f3e
--- /dev/null
+++ b/venv/Lib/site-packages/django_filter-21.1.dist-info/RECORD
@@ -0,0 +1,74 @@
+django_filter-21.1.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
+django_filter-21.1.dist-info/LICENSE,sha256=4UQ8qx2nFmTo4lASXOByK3RcVWDurx7_w9HozSy9mAI,1487
+django_filter-21.1.dist-info/METADATA,sha256=D5IHYcqiWsyxrQggFmRvz3-ncMtjEMdMWfhF1pXOq94,5097
+django_filter-21.1.dist-info/RECORD,,
+django_filter-21.1.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
+django_filter-21.1.dist-info/WHEEL,sha256=ewwEueio1C2XeHTvT17n8dZUJgOvyCWCt0WVNLClP9o,92
+django_filter-21.1.dist-info/top_level.txt,sha256=JVS5v6IKT4Q2Sqv3pRcYCkKZoSdRlH2KalrhTuAenas,15
+django_filters/__init__.py,sha256=x17jQnKLjLiVc0KXqMzaMOiONzH-Krz3WLSPDXA356g,643
+django_filters/__pycache__/__init__.cpython-39.pyc,,
+django_filters/__pycache__/compat.cpython-39.pyc,,
+django_filters/__pycache__/conf.cpython-39.pyc,,
+django_filters/__pycache__/constants.cpython-39.pyc,,
+django_filters/__pycache__/exceptions.cpython-39.pyc,,
+django_filters/__pycache__/fields.cpython-39.pyc,,
+django_filters/__pycache__/filters.cpython-39.pyc,,
+django_filters/__pycache__/filterset.cpython-39.pyc,,
+django_filters/__pycache__/utils.cpython-39.pyc,,
+django_filters/__pycache__/views.cpython-39.pyc,,
+django_filters/__pycache__/widgets.cpython-39.pyc,,
+django_filters/compat.py,sha256=5GeZj6vzL-B56z_KGF4MJq9GkxmNHX_xxGLuPjmqwIQ,545
+django_filters/conf.py,sha256=hfmpXUSr-SSO9kIl4d2KOCyyn7XtooCXmTR3-JqCtK4,3056
+django_filters/constants.py,sha256=LbxdUwNFU2QD8N2XT2Hqcog418gEJk5Ike_Pm08ieOw,64
+django_filters/exceptions.py,sha256=NHK-xlz1XmC_78yNI5-lH1kjwVMb-zrkSegU2Ga8-BY,254
+django_filters/fields.py,sha256=WdnyaoYs6_JmM0sOYnn89xTYLHGgbp_i4A5R_qLIKtQ,9892
+django_filters/filters.py,sha256=uODlEmDv571EqXUHh8Tr_thcus_rAzWbiQlgKrY8F_U,24875
+django_filters/filterset.py,sha256=FBj9WubrBMhYmlsUOtTqJoZLBm69FSOa7URGIzeoNHA,16523
+django_filters/locale/ar/LC_MESSAGES/django.mo,sha256=utzbP4BsdW91KwGgFwyvXVY1uNZ8otdcUDoZZpIZ9Pg,2568
+django_filters/locale/ar/LC_MESSAGES/django.po,sha256=UaO-74kymS4qUcCkGn4VgcoQOsFXoyat40_xT99mT2g,3621
+django_filters/locale/be/LC_MESSAGES/django.mo,sha256=lbp-b9nTHDvBb8ozSkyHWGlmi4X3WyKaObT9GB2fe9E,2819
+django_filters/locale/be/LC_MESSAGES/django.po,sha256=f6SF5hE7bSWaPNBkydB0T55APL4TmlEY7vDlk9bAVL8,3651
+django_filters/locale/bg/LC_MESSAGES/django.mo,sha256=ZPmu82dqvj3yd3-J0KLK-hxfwETzqKmq0c-Anozn5Go,2711
+django_filters/locale/bg/LC_MESSAGES/django.po,sha256=zUPOML-VeE0V-bnW4BNmhL9pewlwkLKfh1MXr8lH2J8,3736
+django_filters/locale/cs/LC_MESSAGES/django.mo,sha256=vZuyiklIF_I3qs9pdhb3OTT2d63aIttgtcHY1b9Gsps,2368
+django_filters/locale/cs/LC_MESSAGES/django.po,sha256=D_4W8R8y5s5x7Hd2xz-Y39zvlN2fr7Df2uCCJ_W-sUw,3144
+django_filters/locale/da/LC_MESSAGES/django.mo,sha256=gPy5CaNJWYbCPqeqb6XPr1uynW9FEn8zV_-85RMJaZc,2166
+django_filters/locale/da/LC_MESSAGES/django.po,sha256=5aCGYuKedqszhcntOw1OUF0_cUtGoXzZB9E1nOoOAnI,3037
+django_filters/locale/de/LC_MESSAGES/django.mo,sha256=IvgqQ0BQ7AiJSmdcGpKWheuLrzrXqs-lbp4Bac2jOdI,2277
+django_filters/locale/de/LC_MESSAGES/django.po,sha256=rbAvo_wdbA494qusvkRGESF4E77jizPafnZhgo9s2aw,3293
+django_filters/locale/el/LC_MESSAGES/django.mo,sha256=2--juTiXF9v6u95krY9VwZCv2cXoJai6CXi4RWpi39w,2836
+django_filters/locale/el/LC_MESSAGES/django.po,sha256=i8h_UnGeVEYAZNXDr6KMqalpq8g3hj6kbtEMcFoqgWE,3966
+django_filters/locale/es/LC_MESSAGES/django.mo,sha256=5KCl_uUwge5RuGStcyMSsVPD6AOunjNvjuE-32PqWis,2279
+django_filters/locale/es/LC_MESSAGES/django.po,sha256=tP1Zuzgz0v84c74PZRy6DuiFyRkN7NOTlMQigEhXL94,3293
+django_filters/locale/es_AR/LC_MESSAGES/django.mo,sha256=OCKAVbT3ct5gf2_t5XsKryjlkIQDYZjC67Oz0j-YE6s,703
+django_filters/locale/es_AR/LC_MESSAGES/django.po,sha256=jI7WMhsSWbTZ7mnLSzB4lsloohr-TtxxPdkNComOZHc,1015
+django_filters/locale/fr/LC_MESSAGES/django.mo,sha256=Bb39Mt6ocOXlcYvT3Om7xMLGNtHEKpzNPQzw7hXqsBE,727
+django_filters/locale/fr/LC_MESSAGES/django.po,sha256=JT34o_10l5Gxyqe5nqDWy8m1TKBsatTTuDE6AqlOhdw,1067
+django_filters/locale/it/LC_MESSAGES/django.mo,sha256=TKIdnZSuYtyCpnl8X9jDyKFuIX6G69CmCvVaWpcuPXM,2268
+django_filters/locale/it/LC_MESSAGES/django.po,sha256=Yo0WtfBI56qq8XWe0QGjYPOw7RexbyJtVRARROYUoYg,3209
+django_filters/locale/pl/LC_MESSAGES/django.mo,sha256=-9taafe4N3mKLdZ4fEXkrj-azO-L4F0fGoxnDgTBuwU,1859
+django_filters/locale/pl/LC_MESSAGES/django.po,sha256=SUzt2qncjYnCmkbgac1BxBz6ZrXhQh2MADSwioCYwCE,3607
+django_filters/locale/pt_BR/LC_MESSAGES/django.mo,sha256=GLakV-03XUsCNKaofuG2fGCBIRGVYEMJiC-kD1UX4D0,2263
+django_filters/locale/pt_BR/LC_MESSAGES/django.po,sha256=R955ohil0dtXI0HU54PhdCalMwb5x4iM2f33a4IHRyg,3217
+django_filters/locale/ru/LC_MESSAGES/django.mo,sha256=1KrtkfLhq0BiDskKFffF5i53pM7Tp-bwsbPDe9F4Co0,2796
+django_filters/locale/ru/LC_MESSAGES/django.po,sha256=ut8nJ7xfrVekgvgHvgTJwQu1CbcVjTHl8gQSuy5yhH8,3818
+django_filters/locale/sk/LC_MESSAGES/django.mo,sha256=em13cqJIPA3JLTp6JXPXuNNeDqJ7uaEuxxqtOvl9PLk,2394
+django_filters/locale/sk/LC_MESSAGES/django.po,sha256=1yaXj0PaV8Ik_SJAA0Wm95GpkYAruZGbIh8rNCkFItw,3386
+django_filters/locale/uk/LC_MESSAGES/django.mo,sha256=zgC01vyDPPS81GiD3C4WeQxtCt4_ift_pU-j_2l_LrU,2912
+django_filters/locale/uk/LC_MESSAGES/django.po,sha256=eX_FYkXmRcVZKYtBoNlZ12fHg8U1FhJ0lAelfI7PcsA,3694
+django_filters/locale/zh_CN/LC_MESSAGES/django.mo,sha256=2aSG7Whwpj7iRY_7QcTV-ReuCm8JKsV-ktlRaAbYC0U,852
+django_filters/locale/zh_CN/LC_MESSAGES/django.po,sha256=9Kj2VQ9TuPgAeQdauY5zR0wOZpkCL1L-GMUpm8fnxT4,1305
+django_filters/rest_framework/__init__.py,sha256=HpNAGIdsBRJSkyM1QmqyOTb7I9VVwoMTbexbD21X6vE,113
+django_filters/rest_framework/__pycache__/__init__.cpython-39.pyc,,
+django_filters/rest_framework/__pycache__/backends.cpython-39.pyc,,
+django_filters/rest_framework/__pycache__/filters.cpython-39.pyc,,
+django_filters/rest_framework/__pycache__/filterset.cpython-39.pyc,,
+django_filters/rest_framework/backends.py,sha256=3XIwJkguiioY4BWdjzcZJIHgN-HUzbNGS-sUP9-lqDo,6182
+django_filters/rest_framework/filters.py,sha256=DXDAE1--_os5SvoFic5VxVfIHvAzidm1Del9A0NCSSA,312
+django_filters/rest_framework/filterset.py,sha256=SHr213z6vpLybwpc9cN8dROBzeGU3Lc2RMhB2Gn--Gs,1200
+django_filters/templates/django_filters/rest_framework/crispy_form.html,sha256=_Mg40d_4sWAuy7_Mzf1HRACbRgeheu0pGXy2UKpzd3s,108
+django_filters/templates/django_filters/rest_framework/form.html,sha256=KoVGtezI-pWnC18jpCKy3vufR23QLpXXooCgmEFXjAA,211
+django_filters/templates/django_filters/widgets/multiwidget.html,sha256=W0RT7BL9-sF-hCA_Ut4MfWaDwE8Z32syJs3anyurceg,118
+django_filters/utils.py,sha256=Gql2Bq2Q7Os6xiASVe_FVizv0PhoaGqYhs7wWGkrQIw,10508
+django_filters/views.py,sha256=xMs37as1DHqM4l279kDs7IBJJKegw3vaddqqGcIjTDo,4181
+django_filters/widgets.py,sha256=wQv7b03h3rAHZFmAMxSy349paRB1WbUu5Vjcv6K1aEI,9126
diff --git a/venv/Lib/site-packages/django_filter-21.1.dist-info/REQUESTED b/venv/Lib/site-packages/django_filter-21.1.dist-info/REQUESTED
new file mode 100644
index 0000000..e69de29
diff --git a/venv/Lib/site-packages/django_filter-21.1.dist-info/WHEEL b/venv/Lib/site-packages/django_filter-21.1.dist-info/WHEEL
new file mode 100644
index 0000000..5bad85f
--- /dev/null
+++ b/venv/Lib/site-packages/django_filter-21.1.dist-info/WHEEL
@@ -0,0 +1,5 @@
+Wheel-Version: 1.0
+Generator: bdist_wheel (0.37.0)
+Root-Is-Purelib: true
+Tag: py3-none-any
+
diff --git a/venv/Lib/site-packages/django_filter-21.1.dist-info/top_level.txt b/venv/Lib/site-packages/django_filter-21.1.dist-info/top_level.txt
new file mode 100644
index 0000000..d34ce38
--- /dev/null
+++ b/venv/Lib/site-packages/django_filter-21.1.dist-info/top_level.txt
@@ -0,0 +1 @@
+django_filters
diff --git a/venv/Lib/site-packages/django_filters/__init__.py b/venv/Lib/site-packages/django_filters/__init__.py
new file mode 100644
index 0000000..1a9aead
--- /dev/null
+++ b/venv/Lib/site-packages/django_filters/__init__.py
@@ -0,0 +1,30 @@
+# flake8: noqa
+import pkgutil
+
+from .filters import *
+from .filterset import FilterSet
+
+# We make the `rest_framework` module available without an additional import.
+# If DRF is not installed, no-op.
+if pkgutil.find_loader('rest_framework') is not None:
+ from . import rest_framework
+del pkgutil
+
+__version__ = '21.1'
+
+
+def parse_version(version):
+ '''
+ '0.1.2.dev1' -> (0, 1, 2, 'dev1')
+ '0.1.2' -> (0, 1, 2)
+ '''
+ v = version.split('.')
+ ret = []
+ for p in v:
+ if p.isdigit():
+ ret.append(int(p))
+ else:
+ ret.append(p)
+ return tuple(ret)
+
+VERSION = parse_version(__version__)
diff --git a/venv/Lib/site-packages/django_filters/compat.py b/venv/Lib/site-packages/django_filters/compat.py
new file mode 100644
index 0000000..fcaa9e9
--- /dev/null
+++ b/venv/Lib/site-packages/django_filters/compat.py
@@ -0,0 +1,25 @@
+from django.conf import settings
+
+# django-crispy-forms is optional
+try:
+ import crispy_forms
+except ImportError:
+ crispy_forms = None
+
+
+def is_crispy():
+ return 'crispy_forms' in settings.INSTALLED_APPS and crispy_forms
+
+
+# coreapi is optional (Note that uritemplate is a dependency of coreapi)
+# Fixes #525 - cannot simply import from rest_framework.compat, due to
+# import issues w/ django-guardian.
+try:
+ import coreapi
+except ImportError:
+ coreapi = None
+
+try:
+ import coreschema
+except ImportError:
+ coreschema = None
diff --git a/venv/Lib/site-packages/django_filters/conf.py b/venv/Lib/site-packages/django_filters/conf.py
new file mode 100644
index 0000000..6ca1767
--- /dev/null
+++ b/venv/Lib/site-packages/django_filters/conf.py
@@ -0,0 +1,109 @@
+from django.conf import settings as dj_settings
+from django.core.signals import setting_changed
+from django.utils.translation import gettext_lazy as _
+
+from .utils import deprecate
+
+DEFAULTS = {
+ 'DISABLE_HELP_TEXT': False,
+
+ 'DEFAULT_LOOKUP_EXPR': 'exact',
+
+ # empty/null choices
+ 'EMPTY_CHOICE_LABEL': '---------',
+ 'NULL_CHOICE_LABEL': None,
+ 'NULL_CHOICE_VALUE': 'null',
+
+ 'VERBOSE_LOOKUPS': {
+ # transforms don't need to be verbose, since their expressions are chained
+ 'date': _('date'),
+ 'year': _('year'),
+ 'month': _('month'),
+ 'day': _('day'),
+ 'week_day': _('week day'),
+ 'hour': _('hour'),
+ 'minute': _('minute'),
+ 'second': _('second'),
+
+ # standard lookups
+ 'exact': '',
+ 'iexact': '',
+ 'contains': _('contains'),
+ 'icontains': _('contains'),
+ 'in': _('is in'),
+ 'gt': _('is greater than'),
+ 'gte': _('is greater than or equal to'),
+ 'lt': _('is less than'),
+ 'lte': _('is less than or equal to'),
+ 'startswith': _('starts with'),
+ 'istartswith': _('starts with'),
+ 'endswith': _('ends with'),
+ 'iendswith': _('ends with'),
+ 'range': _('is in range'),
+ 'isnull': _('is null'),
+ 'regex': _('matches regex'),
+ 'iregex': _('matches regex'),
+ 'search': _('search'),
+
+ # postgres lookups
+ 'contained_by': _('is contained by'),
+ 'overlap': _('overlaps'),
+ 'has_key': _('has key'),
+ 'has_keys': _('has keys'),
+ 'has_any_keys': _('has any keys'),
+ 'trigram_similar': _('search'),
+ },
+}
+
+
+DEPRECATED_SETTINGS = [
+]
+
+
+def is_callable(value):
+ # check for callables, except types
+ return callable(value) and not isinstance(value, type)
+
+
+class Settings:
+
+ def __getattr__(self, name):
+ if name not in DEFAULTS:
+ msg = "'%s' object has no attribute '%s'"
+ raise AttributeError(msg % (self.__class__.__name__, name))
+
+ value = self.get_setting(name)
+
+ if is_callable(value):
+ value = value()
+
+ # Cache the result
+ setattr(self, name, value)
+ return value
+
+ def get_setting(self, setting):
+ django_setting = 'FILTERS_%s' % setting
+
+ if setting in DEPRECATED_SETTINGS and hasattr(dj_settings, django_setting):
+ deprecate("The '%s' setting has been deprecated." % django_setting)
+
+ return getattr(dj_settings, django_setting, DEFAULTS[setting])
+
+ def change_setting(self, setting, value, enter, **kwargs):
+ if not setting.startswith('FILTERS_'):
+ return
+ setting = setting[8:] # strip 'FILTERS_'
+
+ # ensure a valid app setting is being overridden
+ if setting not in DEFAULTS:
+ return
+
+ # if exiting, delete value to repopulate
+ if enter:
+ setattr(self, setting, value)
+ else:
+ delattr(self, setting)
+
+
+settings = Settings()
+setting_changed.connect(settings.change_setting)
diff --git a/venv/Lib/site-packages/django_filters/constants.py b/venv/Lib/site-packages/django_filters/constants.py
new file mode 100644
index 0000000..795d6cc
--- /dev/null
+++ b/venv/Lib/site-packages/django_filters/constants.py
@@ -0,0 +1,5 @@
+
+ALL_FIELDS = '__all__'
+
+
+EMPTY_VALUES = ([], (), {}, '', None)
diff --git a/venv/Lib/site-packages/django_filters/exceptions.py b/venv/Lib/site-packages/django_filters/exceptions.py
new file mode 100644
index 0000000..1d79e4d
--- /dev/null
+++ b/venv/Lib/site-packages/django_filters/exceptions.py
@@ -0,0 +1,9 @@
+
+from django.core.exceptions import FieldError
+
+
+class FieldLookupError(FieldError):
+ def __init__(self, model_field, lookup_expr):
+ super().__init__(
+ "Unsupported lookup '%s' for field '%s'." % (lookup_expr, model_field)
+ )
diff --git a/venv/Lib/site-packages/django_filters/fields.py b/venv/Lib/site-packages/django_filters/fields.py
new file mode 100644
index 0000000..13c9df2
--- /dev/null
+++ b/venv/Lib/site-packages/django_filters/fields.py
@@ -0,0 +1,312 @@
+from collections import namedtuple
+from datetime import datetime, time
+
+from django import forms
+from django.utils.dateparse import parse_datetime
+from django.utils.encoding import force_str
+from django.utils.translation import gettext_lazy as _
+
+from .conf import settings
+from .constants import EMPTY_VALUES
+from .utils import handle_timezone
+from .widgets import (
+ BaseCSVWidget,
+ CSVWidget,
+ DateRangeWidget,
+ LookupChoiceWidget,
+ RangeWidget
+)
+
+
+class RangeField(forms.MultiValueField):
+ widget = RangeWidget
+
+ def __init__(self, fields=None, *args, **kwargs):
+ if fields is None:
+ fields = (
+ forms.DecimalField(),
+ forms.DecimalField())
+ super().__init__(fields, *args, **kwargs)
+
+ def compress(self, data_list):
+ if data_list:
+ return slice(*data_list)
+ return None
+
+
+class DateRangeField(RangeField):
+ widget = DateRangeWidget
+
+ def __init__(self, *args, **kwargs):
+ fields = (
+ forms.DateField(),
+ forms.DateField())
+ super().__init__(fields, *args, **kwargs)
+
+ def compress(self, data_list):
+ if data_list:
+ start_date, stop_date = data_list
+ if start_date:
+ start_date = handle_timezone(
+ datetime.combine(start_date, time.min),
+ False
+ )
+ if stop_date:
+ stop_date = handle_timezone(
+ datetime.combine(stop_date, time.max),
+ False
+ )
+ return slice(start_date, stop_date)
+ return None
+
+
+class DateTimeRangeField(RangeField):
+ widget = DateRangeWidget
+
+ def __init__(self, *args, **kwargs):
+ fields = (
+ forms.DateTimeField(),
+ forms.DateTimeField())
+ super().__init__(fields, *args, **kwargs)
+
+
+class IsoDateTimeRangeField(RangeField):
+ widget = DateRangeWidget
+
+ def __init__(self, *args, **kwargs):
+ fields = (
+ IsoDateTimeField(),
+ IsoDateTimeField())
+ super().__init__(fields, *args, **kwargs)
+
+
+class TimeRangeField(RangeField):
+ widget = DateRangeWidget
+
+ def __init__(self, *args, **kwargs):
+ fields = (
+ forms.TimeField(),
+ forms.TimeField())
+ super().__init__(fields, *args, **kwargs)
+
+
+class Lookup(namedtuple('Lookup', ('value', 'lookup_expr'))):
+ def __new__(cls, value, lookup_expr):
+ if value in EMPTY_VALUES or lookup_expr in EMPTY_VALUES:
+ raise ValueError(
+ "Empty values ([], (), {}, '', None) are not "
+ "valid Lookup arguments. Return None instead."
+ )
+
+ return super().__new__(cls, value, lookup_expr)
+
+
+class LookupChoiceField(forms.MultiValueField):
+ default_error_messages = {
+ 'lookup_required': _('Select a lookup.'),
+ }
+
+ def __init__(self, field, lookup_choices, *args, **kwargs):
+ empty_label = kwargs.pop('empty_label', settings.EMPTY_CHOICE_LABEL)
+ fields = (field, ChoiceField(choices=lookup_choices, empty_label=empty_label))
+ widget = LookupChoiceWidget(widgets=[f.widget for f in fields])
+ kwargs['widget'] = widget
+ kwargs['help_text'] = field.help_text
+ super().__init__(fields, *args, **kwargs)
+
+ def compress(self, data_list):
+ if len(data_list) == 2:
+ value, lookup_expr = data_list
+ if value not in EMPTY_VALUES:
+ if lookup_expr not in EMPTY_VALUES:
+ return Lookup(value=value, lookup_expr=lookup_expr)
+ else:
+ raise forms.ValidationError(
+ self.error_messages['lookup_required'],
+ code='lookup_required')
+ return None
+
+
+class IsoDateTimeField(forms.DateTimeField):
+ """
+ Supports 'iso-8601' date format too which is out the scope of
+ the ``datetime.strptime`` standard library
+
+ # ISO 8601: ``http://www.w3.org/TR/NOTE-datetime``
+
+ Based on Gist example by David Medina https://gist.github.com/copitux/5773821
+ """
+ ISO_8601 = 'iso-8601'
+ input_formats = [ISO_8601]
+
+ def strptime(self, value, format):
+ value = force_str(value)
+
+ if format == self.ISO_8601:
+ parsed = parse_datetime(value)
+ if parsed is None: # Continue with other formats if doesn't match
+ raise ValueError
+ return handle_timezone(parsed)
+ return super().strptime(value, format)
+
+
+class BaseCSVField(forms.Field):
+ """
+ Base field for validating CSV types. Value validation is performed by
+ secondary base classes.
+
+ ex::
+ class IntegerCSVField(BaseCSVField, filters.IntegerField):
+ pass
+
+ """
+ base_widget_class = BaseCSVWidget
+
+ def __init__(self, *args, **kwargs):
+ widget = kwargs.get('widget') or self.widget
+ kwargs['widget'] = self._get_widget_class(widget)
+
+ super().__init__(*args, **kwargs)
+
+ def _get_widget_class(self, widget):
+ # passthrough, allows for override
+ if isinstance(widget, BaseCSVWidget) or (
+ isinstance(widget, type) and
+ issubclass(widget, BaseCSVWidget)):
+ return widget
+
+ # complain since we are unable to reconstruct widget instances
+ assert isinstance(widget, type), \
+ "'%s.widget' must be a widget class, not %s." \
+ % (self.__class__.__name__, repr(widget))
+
+ bases = (self.base_widget_class, widget, )
+ return type(str('CSV%s' % widget.__name__), bases, {})
+
+ def clean(self, value):
+ if value in self.empty_values and self.required:
+ raise forms.ValidationError(self.error_messages['required'], code='required')
+
+ if value is None:
+ return None
+ return [super(BaseCSVField, self).clean(v) for v in value]
+
+
+class BaseRangeField(BaseCSVField):
+ # Force use of text input, as range must always have two inputs. A date
+ # input would only allow a user to input one value and would always fail.
+ widget = CSVWidget
+
+ default_error_messages = {
+ 'invalid_values': _('Range query expects two values.')
+ }
+
+ def clean(self, value):
+ value = super().clean(value)
+
+ assert value is None or isinstance(value, list)
+
+ if value and len(value) != 2:
+ raise forms.ValidationError(
+ self.error_messages['invalid_values'],
+ code='invalid_values')
+
+ return value
+
+
+class ChoiceIterator:
+ # Emulates the behavior of ModelChoiceIterator, but instead wraps
+ # the field's _choices iterable.
+
+ def __init__(self, field, choices):
+ self.field = field
+ self.choices = choices
+
+ def __iter__(self):
+ if self.field.empty_label is not None:
+ yield ("", self.field.empty_label)
+ if self.field.null_label is not None:
+ yield (self.field.null_value, self.field.null_label)
+ yield from self.choices
+
+ def __len__(self):
+ add = 1 if self.field.empty_label is not None else 0
+ add += 1 if self.field.null_label is not None else 0
+ return len(self.choices) + add
+
+
+class ModelChoiceIterator(forms.models.ModelChoiceIterator):
+ # Extends the base ModelChoiceIterator to add in 'null' choice handling.
+ # This is a bit verbose since we have to insert the null choice after the
+ # empty choice, but before the remainder of the choices.
+
+ def __iter__(self):
+ iterable = super().__iter__()
+
+ if self.field.empty_label is not None:
+ yield next(iterable)
+ if self.field.null_label is not None:
+ yield (self.field.null_value, self.field.null_label)
+ yield from iterable
+
+ def __len__(self):
+ add = 1 if self.field.null_label is not None else 0
+ return super().__len__() + add
+
+
+class ChoiceIteratorMixin:
+ def __init__(self, *args, **kwargs):
+ self.null_label = kwargs.pop('null_label', settings.NULL_CHOICE_LABEL)
+ self.null_value = kwargs.pop('null_value', settings.NULL_CHOICE_VALUE)
+
+ super().__init__(*args, **kwargs)
+
+ def _get_choices(self):
+ return super()._get_choices()
+
+ def _set_choices(self, value):
+ super()._set_choices(value)
+ value = self.iterator(self, self._choices)
+
+ self._choices = self.widget.choices = value
+ choices = property(_get_choices, _set_choices)
+
+
+# Unlike their Model* counterparts, forms.ChoiceField and forms.MultipleChoiceField do not set empty_label
+class ChoiceField(ChoiceIteratorMixin, forms.ChoiceField):
+ iterator = ChoiceIterator
+
+ def __init__(self, *args, **kwargs):
+ self.empty_label = kwargs.pop('empty_label', settings.EMPTY_CHOICE_LABEL)
+ super().__init__(*args, **kwargs)
+
+
+class MultipleChoiceField(ChoiceIteratorMixin, forms.MultipleChoiceField):
+ iterator = ChoiceIterator
+
+ def __init__(self, *args, **kwargs):
+ self.empty_label = None
+ super().__init__(*args, **kwargs)
+
+
+class ModelChoiceField(ChoiceIteratorMixin, forms.ModelChoiceField):
+ iterator = ModelChoiceIterator
+
+ def to_python(self, value):
+ # bypass the queryset value check
+ if self.null_label is not None and value == self.null_value:
+ return value
+ return super().to_python(value)
+
+
+class ModelMultipleChoiceField(ChoiceIteratorMixin, forms.ModelMultipleChoiceField):
+ iterator = ModelChoiceIterator
+
+ def _check_values(self, value):
+ null = self.null_label is not None and value and self.null_value in value
+ if null: # remove the null value and any potential duplicates
+ value = [v for v in value if v != self.null_value]
+
+ result = list(super()._check_values(value))
+ result += [self.null_value] if null else []
+ return result
diff --git a/venv/Lib/site-packages/django_filters/filters.py b/venv/Lib/site-packages/django_filters/filters.py
new file mode 100644
index 0000000..4f6ebe3
--- /dev/null
+++ b/venv/Lib/site-packages/django_filters/filters.py
@@ -0,0 +1,804 @@
+from collections import OrderedDict
+from datetime import timedelta
+
+from django import forms
+from django.core.validators import MaxValueValidator
+from django.db.models import Q
+from django.db.models.constants import LOOKUP_SEP
+from django.forms.utils import pretty_name
+from django.utils.itercompat import is_iterable
+from django.utils.timezone import now
+from django.utils.translation import gettext_lazy as _
+
+from .conf import settings
+from .constants import EMPTY_VALUES
+from .fields import (
+ BaseCSVField,
+ BaseRangeField,
+ ChoiceField,
+ DateRangeField,
+ DateTimeRangeField,
+ IsoDateTimeField,
+ IsoDateTimeRangeField,
+ LookupChoiceField,
+ ModelChoiceField,
+ ModelMultipleChoiceField,
+ MultipleChoiceField,
+ RangeField,
+ TimeRangeField
+)
+from .utils import get_model_field, label_for_filter
+
+__all__ = [
+ 'AllValuesFilter',
+ 'AllValuesMultipleFilter',
+ 'BaseCSVFilter',
+ 'BaseInFilter',
+ 'BaseRangeFilter',
+ 'BooleanFilter',
+ 'CharFilter',
+ 'ChoiceFilter',
+ 'DateFilter',
+ 'DateFromToRangeFilter',
+ 'DateRangeFilter',
+ 'DateTimeFilter',
+ 'DateTimeFromToRangeFilter',
+ 'DurationFilter',
+ 'Filter',
+ 'IsoDateTimeFilter',
+ 'IsoDateTimeFromToRangeFilter',
+ 'LookupChoiceFilter',
+ 'ModelChoiceFilter',
+ 'ModelMultipleChoiceFilter',
+ 'MultipleChoiceFilter',
+ 'NumberFilter',
+ 'NumericRangeFilter',
+ 'OrderingFilter',
+ 'RangeFilter',
+ 'TimeFilter',
+ 'TimeRangeFilter',
+ 'TypedChoiceFilter',
+ 'TypedMultipleChoiceFilter',
+ 'UUIDFilter',
+]
+
+
+class Filter:
+ creation_counter = 0
+ field_class = forms.Field
+
+ def __init__(self, field_name=None, lookup_expr=None, *, label=None,
+ method=None, distinct=False, exclude=False, **kwargs):
+ if lookup_expr is None:
+ lookup_expr = settings.DEFAULT_LOOKUP_EXPR
+ self.field_name = field_name
+ self.lookup_expr = lookup_expr
+ self.label = label
+ self.method = method
+ self.distinct = distinct
+ self.exclude = exclude
+
+ self.extra = kwargs
+ self.extra.setdefault('required', False)
+
+ self.creation_counter = Filter.creation_counter
+ Filter.creation_counter += 1
+
+ def get_method(self, qs):
+ """Return filter method based on whether we're excluding
+ or simply filtering.
+ """
+ return qs.exclude if self.exclude else qs.filter
+
+ def method():
+ """
+ Filter method needs to be lazily resolved, as it may be dependent on
+ the 'parent' FilterSet.
+ """
+ def fget(self):
+ return self._method
+
+ def fset(self, value):
+ self._method = value
+
+ # clear existing FilterMethod
+ if isinstance(self.filter, FilterMethod):
+ del self.filter
+
+ # override filter w/ FilterMethod.
+ if value is not None:
+ self.filter = FilterMethod(self)
+
+ return locals()
+ method = property(**method())
+
+ def label():
+ def fget(self):
+ if self._label is None and hasattr(self, 'model'):
+ self._label = label_for_filter(
+ self.model, self.field_name, self.lookup_expr, self.exclude
+ )
+ return self._label
+
+ def fset(self, value):
+ self._label = value
+
+ return locals()
+ label = property(**label())
+
+ @property
+ def field(self):
+ if not hasattr(self, '_field'):
+ field_kwargs = self.extra.copy()
+
+ if settings.DISABLE_HELP_TEXT:
+ field_kwargs.pop('help_text', None)
+
+ self._field = self.field_class(label=self.label, **field_kwargs)
+ return self._field
+
+ def filter(self, qs, value):
+ if value in EMPTY_VALUES:
+ return qs
+ if self.distinct:
+ qs = qs.distinct()
+ lookup = '%s__%s' % (self.field_name, self.lookup_expr)
+ qs = self.get_method(qs)(**{lookup: value})
+ return qs
+
+
+class CharFilter(Filter):
+ field_class = forms.CharField
+
+
+class BooleanFilter(Filter):
+ field_class = forms.NullBooleanField
+
+
+class ChoiceFilter(Filter):
+ field_class = ChoiceField
+
+ def __init__(self, *args, **kwargs):
+ self.null_value = kwargs.get('null_value', settings.NULL_CHOICE_VALUE)
+ super().__init__(*args, **kwargs)
+
+ def filter(self, qs, value):
+ if value != self.null_value:
+ return super().filter(qs, value)
+
+ qs = self.get_method(qs)(**{'%s__%s' % (self.field_name, self.lookup_expr): None})
+ return qs.distinct() if self.distinct else qs
+
+
+class TypedChoiceFilter(Filter):
+ field_class = forms.TypedChoiceField
+
+
+class UUIDFilter(Filter):
+ field_class = forms.UUIDField
+
+
+class MultipleChoiceFilter(Filter):
+ """
+ This filter performs OR(by default) or AND(using conjoined=True) query
+ on the selected options.
+
+ Advanced usage
+ --------------
+ Depending on your application logic, when all or no choices are selected,
+ filtering may be a no-operation. In this case you may wish to avoid the
+ filtering overhead, particularly if using a `distinct` call.
+
+ You can override `get_filter_predicate` to use a custom filter.
+ By default it will use the filter's name for the key, and the value will
+ be the model object - or in case of passing in `to_field_name` the
+ value of that attribute on the model.
+
+ Set `always_filter` to `False` after instantiation to enable the default
+ `is_noop` test. You can override `is_noop` if you need a different test
+ for your application.
+
+ `distinct` defaults to `True` as to-many relationships will generally
+ require this.
+ """
+ field_class = MultipleChoiceField
+
+ always_filter = True
+
+ def __init__(self, *args, **kwargs):
+ kwargs.setdefault('distinct', True)
+ self.conjoined = kwargs.pop('conjoined', False)
+ self.null_value = kwargs.get('null_value', settings.NULL_CHOICE_VALUE)
+ super().__init__(*args, **kwargs)
+
+ def is_noop(self, qs, value):
+ """
+ Return `True` to short-circuit unnecessary and potentially slow
+ filtering.
+ """
+ if self.always_filter:
+ return False
+
+ # A reasonable default for being a noop...
+ if self.extra.get('required') and len(value) == len(self.field.choices):
+ return True
+
+ return False
+
+ def filter(self, qs, value):
+ if not value:
+ # Even though not a noop, no point filtering if empty.
+ return qs
+
+ if self.is_noop(qs, value):
+ return qs
+
+ if not self.conjoined:
+ q = Q()
+ for v in set(value):
+ if v == self.null_value:
+ v = None
+ predicate = self.get_filter_predicate(v)
+ if self.conjoined:
+ qs = self.get_method(qs)(**predicate)
+ else:
+ q |= Q(**predicate)
+
+ if not self.conjoined:
+ qs = self.get_method(qs)(q)
+
+ return qs.distinct() if self.distinct else qs
+
+ def get_filter_predicate(self, v):
+ name = self.field_name
+ if name and self.lookup_expr != settings.DEFAULT_LOOKUP_EXPR:
+ name = LOOKUP_SEP.join([name, self.lookup_expr])
+ try:
+ return {name: getattr(v, self.field.to_field_name)}
+ except (AttributeError, TypeError):
+ return {name: v}
+
+
+class TypedMultipleChoiceFilter(MultipleChoiceFilter):
+ field_class = forms.TypedMultipleChoiceField
+
+
+class DateFilter(Filter):
+ field_class = forms.DateField
+
+
+class DateTimeFilter(Filter):
+ field_class = forms.DateTimeField
+
+
+class IsoDateTimeFilter(DateTimeFilter):
+ """
+ Uses IsoDateTimeField to support filtering on ISO 8601 formatted datetimes.
+
+ For context see:
+
+ * https://code.djangoproject.com/ticket/23448
+ * https://github.com/encode/django-rest-framework/issues/1338
+ * https://github.com/carltongibson/django-filter/pull/264
+ """
+ field_class = IsoDateTimeField
+
+
+class TimeFilter(Filter):
+ field_class = forms.TimeField
+
+
+class DurationFilter(Filter):
+ field_class = forms.DurationField
+
+
+class QuerySetRequestMixin:
+ """
+ Add callable functionality to filters that support the ``queryset``
+ argument. If the ``queryset`` is callable, then it **must** accept the
+ ``request`` object as a single argument.
+
+ This is useful for filtering querysets by properties on the ``request``
+ object, such as the user.
+
+ Example::
+
+ def departments(request):
+ company = request.user.company
+ return company.department_set.all()
+
+ class EmployeeFilter(filters.FilterSet):
+ department = filters.ModelChoiceFilter(queryset=departments)
+ ...
+
+ The above example restricts the set of departments to those in the logged-in
+ user's associated company.
+
+ """
+ def __init__(self, *args, **kwargs):
+ self.queryset = kwargs.get('queryset')
+ super().__init__(*args, **kwargs)
+
+ def get_request(self):
+ try:
+ return self.parent.request
+ except AttributeError:
+ return None
+
+ def get_queryset(self, request):
+ queryset = self.queryset
+
+ if callable(queryset):
+ return queryset(request)
+ return queryset
+
+ @property
+ def field(self):
+ request = self.get_request()
+ queryset = self.get_queryset(request)
+
+ if queryset is not None:
+ self.extra['queryset'] = queryset
+
+ return super().field
+
+
+class ModelChoiceFilter(QuerySetRequestMixin, ChoiceFilter):
+ field_class = ModelChoiceField
+
+ def __init__(self, *args, **kwargs):
+ kwargs.setdefault('empty_label', settings.EMPTY_CHOICE_LABEL)
+ super().__init__(*args, **kwargs)
+
+
+class ModelMultipleChoiceFilter(QuerySetRequestMixin, MultipleChoiceFilter):
+ field_class = ModelMultipleChoiceField
+
+
+class NumberFilter(Filter):
+ field_class = forms.DecimalField
+
+ def get_max_validator(self):
+ """
+ Return a MaxValueValidator for the field, or None to disable.
+ """
+ return MaxValueValidator(1e50)
+
+ @property
+ def field(self):
+ if not hasattr(self, '_field'):
+ field = super().field
+ max_validator = self.get_max_validator()
+ if max_validator:
+ field.validators.append(max_validator)
+
+ self._field = field
+ return self._field
+
+
+class NumericRangeFilter(Filter):
+ field_class = RangeField
+
+ def filter(self, qs, value):
+ if value:
+ if value.start is not None and value.stop is not None:
+ value = (value.start, value.stop)
+ elif value.start is not None:
+ self.lookup_expr = 'startswith'
+ value = value.start
+ elif value.stop is not None:
+ self.lookup_expr = 'endswith'
+ value = value.stop
+
+ return super().filter(qs, value)
+
+
+class RangeFilter(Filter):
+ field_class = RangeField
+
+ def filter(self, qs, value):
+ if value:
+ if value.start is not None and value.stop is not None:
+ self.lookup_expr = 'range'
+ value = (value.start, value.stop)
+ elif value.start is not None:
+ self.lookup_expr = 'gte'
+ value = value.start
+ elif value.stop is not None:
+ self.lookup_expr = 'lte'
+ value = value.stop
+
+ return super().filter(qs, value)
+
+
+def _truncate(dt):
+ return dt.date()
+
+
+class DateRangeFilter(ChoiceFilter):
+ choices = [
+ ('today', _('Today')),
+ ('yesterday', _('Yesterday')),
+ ('week', _('Past 7 days')),
+ ('month', _('This month')),
+ ('year', _('This year')),
+ ]
+
+ filters = {
+ 'today': lambda qs, name: qs.filter(**{
+ '%s__year' % name: now().year,
+ '%s__month' % name: now().month,
+ '%s__day' % name: now().day
+ }),
+ 'yesterday': lambda qs, name: qs.filter(**{
+ '%s__year' % name: (now() - timedelta(days=1)).year,
+ '%s__month' % name: (now() - timedelta(days=1)).month,
+ '%s__day' % name: (now() - timedelta(days=1)).day,
+ }),
+ 'week': lambda qs, name: qs.filter(**{
+ '%s__gte' % name: _truncate(now() - timedelta(days=7)),
+ '%s__lt' % name: _truncate(now() + timedelta(days=1)),
+ }),
+ 'month': lambda qs, name: qs.filter(**{
+ '%s__year' % name: now().year,
+ '%s__month' % name: now().month
+ }),
+ 'year': lambda qs, name: qs.filter(**{
+ '%s__year' % name: now().year,
+ }),
+ }
+
+ def __init__(self, choices=None, filters=None, *args, **kwargs):
+ if choices is not None:
+ self.choices = choices
+ if filters is not None:
+ self.filters = filters
+
+ unique = set([x[0] for x in self.choices]) ^ set(self.filters)
+ assert not unique, \
+ "Keys must be present in both 'choices' and 'filters'. Missing keys: " \
+ "'%s'" % ', '.join(sorted(unique))
+
+ # TODO: remove assertion in 2.1
+ assert not hasattr(self, 'options'), \
+ "The 'options' attribute has been replaced by 'choices' and 'filters'. " \
+ "See: https://django-filter.readthedocs.io/en/main/guide/migration.html"
+
+ # null choice not relevant
+ kwargs.setdefault('null_label', None)
+ super().__init__(choices=self.choices, *args, **kwargs)
+
+ def filter(self, qs, value):
+ if not value:
+ return qs
+
+ assert value in self.filters
+
+ qs = self.filters[value](qs, self.field_name)
+ return qs.distinct() if self.distinct else qs
+
+
+class DateFromToRangeFilter(RangeFilter):
+ field_class = DateRangeField
+
+
+class DateTimeFromToRangeFilter(RangeFilter):
+ field_class = DateTimeRangeField
+
+
+class IsoDateTimeFromToRangeFilter(RangeFilter):
+ field_class = IsoDateTimeRangeField
+
+
+class TimeRangeFilter(RangeFilter):
+ field_class = TimeRangeField
+
+
+class AllValuesFilter(ChoiceFilter):
+ @property
+ def field(self):
+ qs = self.model._default_manager.distinct()
+ qs = qs.order_by(self.field_name).values_list(self.field_name, flat=True)
+ self.extra['choices'] = [(o, o) for o in qs]
+ return super().field
+
+
+class AllValuesMultipleFilter(MultipleChoiceFilter):
+ @property
+ def field(self):
+ qs = self.model._default_manager.distinct()
+ qs = qs.order_by(self.field_name).values_list(self.field_name, flat=True)
+ self.extra['choices'] = [(o, o) for o in qs]
+ return super().field
+
+
+class BaseCSVFilter(Filter):
+ """
+ Base class for CSV type filters, such as IN and RANGE.
+ """
+ base_field_class = BaseCSVField
+
+ def __init__(self, *args, **kwargs):
+ kwargs.setdefault('help_text', _('Multiple values may be separated by commas.'))
+ super().__init__(*args, **kwargs)
+
+ class ConcreteCSVField(self.base_field_class, self.field_class):
+ pass
+ ConcreteCSVField.__name__ = self._field_class_name(
+ self.field_class, self.lookup_expr
+ )
+
+ self.field_class = ConcreteCSVField
+
+ @classmethod
+ def _field_class_name(cls, field_class, lookup_expr):
+ """
+ Generate a suitable class name for the concrete field class. This is not
+ completely reliable, as not all field class names are of the format
+ Field.
+
+ ex::
+
+ BaseCSVFilter._field_class_name(DateTimeField, 'year__in')
+
+ returns 'DateTimeYearInField'
+
+ """
+ # DateTimeField => DateTime
+ type_name = field_class.__name__
+ if type_name.endswith('Field'):
+ type_name = type_name[:-5]
+
+ # year__in => YearIn
+ parts = lookup_expr.split(LOOKUP_SEP)
+ expression_name = ''.join(p.capitalize() for p in parts)
+
+ # DateTimeYearInField
+ return str('%s%sField' % (type_name, expression_name))
+
+
+class BaseInFilter(BaseCSVFilter):
+
+ def __init__(self, *args, **kwargs):
+ kwargs.setdefault('lookup_expr', 'in')
+ super().__init__(*args, **kwargs)
+
+
+class BaseRangeFilter(BaseCSVFilter):
+ base_field_class = BaseRangeField
+
+ def __init__(self, *args, **kwargs):
+ kwargs.setdefault('lookup_expr', 'range')
+ super().__init__(*args, **kwargs)
+
+
+class LookupChoiceFilter(Filter):
+ """
+ A combined filter that allows users to select the lookup expression from a dropdown.
+
+ * ``lookup_choices`` is an optional argument that accepts multiple input
+ formats, and is ultimately normlized as the choices used in the lookup
+ dropdown. See ``.get_lookup_choices()`` for more information.
+
+ * ``field_class`` is an optional argument that allows you to set the inner
+ form field class used to validate the value. Default: ``forms.CharField``
+
+ ex::
+
+ price = django_filters.LookupChoiceFilter(
+ field_class=forms.DecimalField,
+ lookup_choices=[
+ ('exact', 'Equals'),
+ ('gt', 'Greater than'),
+ ('lt', 'Less than'),
+ ]
+ )
+
+ """
+ field_class = forms.CharField
+ outer_class = LookupChoiceField
+
+ def __init__(self, field_name=None, lookup_choices=None, field_class=None, **kwargs):
+ self.empty_label = kwargs.pop('empty_label', settings.EMPTY_CHOICE_LABEL)
+
+ super(LookupChoiceFilter, self).__init__(field_name=field_name, **kwargs)
+
+ self.lookup_choices = lookup_choices
+ if field_class is not None:
+ self.field_class = field_class
+
+ @classmethod
+ def normalize_lookup(cls, lookup):
+ """
+ Normalize the lookup into a tuple of ``(lookup expression, display value)``
+
+ If the ``lookup`` is already a tuple, the tuple is not altered.
+ If the ``lookup`` is a string, a tuple is returned with the lookup
+ expression used as the basis for the display value.
+
+ ex::
+
+ >>> LookupChoiceFilter.normalize_lookup(('exact', 'Equals'))
+ ('exact', 'Equals')
+
+ >>> LookupChoiceFilter.normalize_lookup('has_key')
+ ('has_key', 'Has key')
+
+ """
+ if isinstance(lookup, str):
+ return (lookup, pretty_name(lookup))
+ return (lookup[0], lookup[1])
+
+ def get_lookup_choices(self):
+ """
+ Get the lookup choices in a format suitable for ``django.forms.ChoiceField``.
+ If the filter is initialized with ``lookup_choices``, this value is normalized
+ and passed to the underlying ``LookupChoiceField``. If no choices are provided,
+ they are generated from the corresponding model field's registered lookups.
+ """
+ lookups = self.lookup_choices
+ if lookups is None:
+ field = get_model_field(self.model, self.field_name)
+ lookups = field.get_lookups()
+
+ return [self.normalize_lookup(lookup) for lookup in lookups]
+
+ @property
+ def field(self):
+ if not hasattr(self, '_field'):
+ inner_field = super().field
+ lookups = self.get_lookup_choices()
+
+ self._field = self.outer_class(
+ inner_field, lookups,
+ label=self.label,
+ empty_label=self.empty_label,
+ required=self.extra['required'],
+ )
+
+ return self._field
+
+ def filter(self, qs, lookup):
+ if not lookup:
+ return super().filter(qs, None)
+
+ self.lookup_expr = lookup.lookup_expr
+ return super().filter(qs, lookup.value)
+
+
+class OrderingFilter(BaseCSVFilter, ChoiceFilter):
+ """
+ Enable queryset ordering. As an extension of ``ChoiceFilter`` it accepts
+ two additional arguments that are used to build the ordering choices.
+
+ * ``fields`` is a mapping of {model field name: parameter name}. The
+ parameter names are exposed in the choices and mask/alias the field
+ names used in the ``order_by()`` call. Similar to field ``choices``,
+ ``fields`` accepts the 'list of two-tuples' syntax that retains order.
+ ``fields`` may also just be an iterable of strings. In this case, the
+ field names simply double as the exposed parameter names.
+
+ * ``field_labels`` is an optional argument that allows you to customize
+ the display label for the corresponding parameter. It accepts a mapping
+ of {field name: human readable label}. Keep in mind that the key is the
+ field name, and not the exposed parameter name.
+
+ Additionally, you can just provide your own ``choices`` if you require
+ explicit control over the exposed options. For example, when you might
+ want to disable descending sort options.
+
+ This filter is also CSV-based, and accepts multiple ordering params. The
+ default select widget does not enable the use of this, but it is useful
+ for APIs.
+
+ """
+ descending_fmt = _('%s (descending)')
+
+ def __init__(self, *args, **kwargs):
+ """
+ ``fields`` may be either a mapping or an iterable.
+ ``field_labels`` must be a map of field names to display labels
+ """
+ fields = kwargs.pop('fields', {})
+ fields = self.normalize_fields(fields)
+ field_labels = kwargs.pop('field_labels', {})
+
+ self.param_map = {v: k for k, v in fields.items()}
+
+ if 'choices' not in kwargs:
+ kwargs['choices'] = self.build_choices(fields, field_labels)
+
+ kwargs.setdefault('label', _('Ordering'))
+ kwargs.setdefault('help_text', '')
+ kwargs.setdefault('null_label', None)
+ super().__init__(*args, **kwargs)
+
+ def get_ordering_value(self, param):
+ descending = param.startswith('-')
+ param = param[1:] if descending else param
+ field_name = self.param_map.get(param, param)
+
+ return "-%s" % field_name if descending else field_name
+
+ def filter(self, qs, value):
+ if value in EMPTY_VALUES:
+ return qs
+
+ ordering = [self.get_ordering_value(param) for param in value]
+ return qs.order_by(*ordering)
+
+ @classmethod
+ def normalize_fields(cls, fields):
+ """
+ Normalize the fields into an ordered map of {field name: param name}
+ """
+ # fields is a mapping, copy into new OrderedDict
+ if isinstance(fields, dict):
+ return OrderedDict(fields)
+
+ # convert iterable of values => iterable of pairs (field name, param name)
+ assert is_iterable(fields), \
+ "'fields' must be an iterable (e.g., a list, tuple, or mapping)."
+
+ # fields is an iterable of field names
+ assert all(isinstance(field, str) or
+ is_iterable(field) and len(field) == 2 # may need to be wrapped in parens
+ for field in fields), \
+ "'fields' must contain strings or (field name, param name) pairs."
+
+ return OrderedDict([
+ (f, f) if isinstance(f, str) else f for f in fields
+ ])
+
+ def build_choices(self, fields, labels):
+ ascending = [
+ (param, labels.get(field, _(pretty_name(param))))
+ for field, param in fields.items()
+ ]
+ descending = [
+ ('-%s' % param, labels.get('-%s' % param, self.descending_fmt % label))
+ for param, label in ascending
+ ]
+
+ # interleave the ascending and descending choices
+ return [val for pair in zip(ascending, descending) for val in pair]
+
+
+class FilterMethod:
+ """
+ This helper is used to override Filter.filter() when a 'method' argument
+ is passed. It proxies the call to the actual method on the filter's parent.
+ """
+ def __init__(self, filter_instance):
+ self.f = filter_instance
+
+ def __call__(self, qs, value):
+ if value in EMPTY_VALUES:
+ return qs
+
+ return self.method(qs, self.f.field_name, value)
+
+ @property
+ def method(self):
+ """
+ Resolve the method on the parent filterset.
+ """
+ instance = self.f
+
+ # noop if 'method' is a function
+ if callable(instance.method):
+ return instance.method
+
+ # otherwise, method is the name of a method on the parent FilterSet.
+ assert hasattr(instance, 'parent'), \
+ "Filter '%s' must have a parent FilterSet to find '.%s()'" % \
+ (instance.field_name, instance.method)
+
+ parent = instance.parent
+ method = getattr(parent, instance.method, None)
+
+ assert callable(method), \
+ "Expected parent FilterSet '%s.%s' to have a '.%s()' method." % \
+ (parent.__class__.__module__, parent.__class__.__name__, instance.method)
+
+ return method
diff --git a/venv/Lib/site-packages/django_filters/filterset.py b/venv/Lib/site-packages/django_filters/filterset.py
new file mode 100644
index 0000000..9c2a4f7
--- /dev/null
+++ b/venv/Lib/site-packages/django_filters/filterset.py
@@ -0,0 +1,470 @@
+import copy
+from collections import OrderedDict
+
+from django import forms
+from django.db import models
+from django.db.models.constants import LOOKUP_SEP
+from django.db.models.fields.related import (
+ ManyToManyRel,
+ ManyToOneRel,
+ OneToOneRel
+)
+
+from .conf import settings
+from .constants import ALL_FIELDS
+from .filters import (
+ BaseInFilter,
+ BaseRangeFilter,
+ BooleanFilter,
+ CharFilter,
+ ChoiceFilter,
+ DateFilter,
+ DateTimeFilter,
+ DurationFilter,
+ Filter,
+ ModelChoiceFilter,
+ ModelMultipleChoiceFilter,
+ NumberFilter,
+ TimeFilter,
+ UUIDFilter
+)
+from .utils import (
+ get_all_model_fields,
+ get_model_field,
+ resolve_field,
+ try_dbfield
+)
+
+
+def remote_queryset(field):
+ """
+ Get the queryset for the other side of a relationship. This works
+ for both `RelatedField`s and `ForeignObjectRel`s.
+ """
+ model = field.related_model
+
+ # Reverse relationships do not have choice limits
+ if not hasattr(field, 'get_limit_choices_to'):
+ return model._default_manager.all()
+
+ limit_choices_to = field.get_limit_choices_to()
+ return model._default_manager.complex_filter(limit_choices_to)
+
+
+class FilterSetOptions:
+ def __init__(self, options=None):
+ self.model = getattr(options, 'model', None)
+ self.fields = getattr(options, 'fields', None)
+ self.exclude = getattr(options, 'exclude', None)
+
+ self.filter_overrides = getattr(options, 'filter_overrides', {})
+
+ self.form = getattr(options, 'form', forms.Form)
+
+
+class FilterSetMetaclass(type):
+ def __new__(cls, name, bases, attrs):
+ attrs['declared_filters'] = cls.get_declared_filters(bases, attrs)
+
+ new_class = super().__new__(cls, name, bases, attrs)
+ new_class._meta = FilterSetOptions(getattr(new_class, 'Meta', None))
+ new_class.base_filters = new_class.get_filters()
+
+ # TODO: remove assertion in 2.1
+ assert not hasattr(new_class, 'filter_for_reverse_field'), (
+ "`%(cls)s.filter_for_reverse_field` has been removed. "
+ "`%(cls)s.filter_for_field` now generates filters for reverse fields. "
+ "See: https://django-filter.readthedocs.io/en/main/guide/migration.html"
+ % {'cls': new_class.__name__}
+ )
+
+ return new_class
+
+ @classmethod
+ def get_declared_filters(cls, bases, attrs):
+ filters = [
+ (filter_name, attrs.pop(filter_name))
+ for filter_name, obj in list(attrs.items())
+ if isinstance(obj, Filter)
+ ]
+
+ # Default the `filter.field_name` to the attribute name on the filterset
+ for filter_name, f in filters:
+ if getattr(f, 'field_name', None) is None:
+ f.field_name = filter_name
+
+ filters.sort(key=lambda x: x[1].creation_counter)
+
+ # Ensures a base class field doesn't override cls attrs, and maintains
+ # field precedence when inheriting multiple parents. e.g. if there is a
+ # class C(A, B), and A and B both define 'field', use 'field' from A.
+ known = set(attrs)
+
+ def visit(name):
+ known.add(name)
+ return name
+
+ base_filters = [
+ (visit(name), f)
+ for base in bases if hasattr(base, 'declared_filters')
+ for name, f in base.declared_filters.items() if name not in known
+ ]
+
+ return OrderedDict(base_filters + filters)
+
+
+FILTER_FOR_DBFIELD_DEFAULTS = {
+ models.AutoField: {'filter_class': NumberFilter},
+ models.CharField: {'filter_class': CharFilter},
+ models.TextField: {'filter_class': CharFilter},
+ models.BooleanField: {'filter_class': BooleanFilter},
+ models.DateField: {'filter_class': DateFilter},
+ models.DateTimeField: {'filter_class': DateTimeFilter},
+ models.TimeField: {'filter_class': TimeFilter},
+ models.DurationField: {'filter_class': DurationFilter},
+ models.DecimalField: {'filter_class': NumberFilter},
+ models.SmallIntegerField: {'filter_class': NumberFilter},
+ models.IntegerField: {'filter_class': NumberFilter},
+ models.PositiveIntegerField: {'filter_class': NumberFilter},
+ models.PositiveSmallIntegerField: {'filter_class': NumberFilter},
+ models.FloatField: {'filter_class': NumberFilter},
+ models.NullBooleanField: {'filter_class': BooleanFilter},
+ models.SlugField: {'filter_class': CharFilter},
+ models.EmailField: {'filter_class': CharFilter},
+ models.FilePathField: {'filter_class': CharFilter},
+ models.URLField: {'filter_class': CharFilter},
+ models.GenericIPAddressField: {'filter_class': CharFilter},
+ models.CommaSeparatedIntegerField: {'filter_class': CharFilter},
+ models.UUIDField: {'filter_class': UUIDFilter},
+
+ # Forward relationships
+ models.OneToOneField: {
+ 'filter_class': ModelChoiceFilter,
+ 'extra': lambda f: {
+ 'queryset': remote_queryset(f),
+ 'to_field_name': f.remote_field.field_name,
+ 'null_label': settings.NULL_CHOICE_LABEL if f.null else None,
+ }
+ },
+ models.ForeignKey: {
+ 'filter_class': ModelChoiceFilter,
+ 'extra': lambda f: {
+ 'queryset': remote_queryset(f),
+ 'to_field_name': f.remote_field.field_name,
+ 'null_label': settings.NULL_CHOICE_LABEL if f.null else None,
+ }
+ },
+ models.ManyToManyField: {
+ 'filter_class': ModelMultipleChoiceFilter,
+ 'extra': lambda f: {
+ 'queryset': remote_queryset(f),
+ }
+ },
+
+ # Reverse relationships
+ OneToOneRel: {
+ 'filter_class': ModelChoiceFilter,
+ 'extra': lambda f: {
+ 'queryset': remote_queryset(f),
+ 'null_label': settings.NULL_CHOICE_LABEL if f.null else None,
+ }
+ },
+ ManyToOneRel: {
+ 'filter_class': ModelMultipleChoiceFilter,
+ 'extra': lambda f: {
+ 'queryset': remote_queryset(f),
+ }
+ },
+ ManyToManyRel: {
+ 'filter_class': ModelMultipleChoiceFilter,
+ 'extra': lambda f: {
+ 'queryset': remote_queryset(f),
+ }
+ },
+}
+
+
+class BaseFilterSet:
+ FILTER_DEFAULTS = FILTER_FOR_DBFIELD_DEFAULTS
+
+ def __init__(self, data=None, queryset=None, *, request=None, prefix=None):
+ if queryset is None:
+ queryset = self._meta.model._default_manager.all()
+ model = queryset.model
+
+ self.is_bound = data is not None
+ self.data = data or {}
+ self.queryset = queryset
+ self.request = request
+ self.form_prefix = prefix
+
+ self.filters = copy.deepcopy(self.base_filters)
+
+ # propagate the model and filterset to the filters
+ for filter_ in self.filters.values():
+ filter_.model = model
+ filter_.parent = self
+
+ def is_valid(self):
+ """
+ Return True if the underlying form has no errors, or False otherwise.
+ """
+ return self.is_bound and self.form.is_valid()
+
+ @property
+ def errors(self):
+ """
+ Return an ErrorDict for the data provided for the underlying form.
+ """
+ return self.form.errors
+
+ def filter_queryset(self, queryset):
+ """
+ Filter the queryset with the underlying form's `cleaned_data`. You must
+ call `is_valid()` or `errors` before calling this method.
+
+ This method should be overridden if additional filtering needs to be
+ applied to the queryset before it is cached.
+ """
+ for name, value in self.form.cleaned_data.items():
+ queryset = self.filters[name].filter(queryset, value)
+ assert isinstance(queryset, models.QuerySet), \
+ "Expected '%s.%s' to return a QuerySet, but got a %s instead." \
+ % (type(self).__name__, name, type(queryset).__name__)
+ return queryset
+
+ @property
+ def qs(self):
+ if not hasattr(self, '_qs'):
+ qs = self.queryset.all()
+ if self.is_bound:
+ # ensure form validation before filtering
+ self.errors
+ qs = self.filter_queryset(qs)
+ self._qs = qs
+ return self._qs
+
+ def get_form_class(self):
+ """
+ Returns a django Form suitable of validating the filterset data.
+
+ This method should be overridden if the form class needs to be
+ customized relative to the filterset instance.
+ """
+ fields = OrderedDict([
+ (name, filter_.field)
+ for name, filter_ in self.filters.items()])
+
+ return type(str('%sForm' % self.__class__.__name__),
+ (self._meta.form,), fields)
+
+ @property
+ def form(self):
+ if not hasattr(self, '_form'):
+ Form = self.get_form_class()
+ if self.is_bound:
+ self._form = Form(self.data, prefix=self.form_prefix)
+ else:
+ self._form = Form(prefix=self.form_prefix)
+ return self._form
+
+ @classmethod
+ def get_fields(cls):
+ """
+ Resolve the 'fields' argument that should be used for generating filters on the
+ filterset. This is 'Meta.fields' sans the fields in 'Meta.exclude'.
+ """
+ model = cls._meta.model
+ fields = cls._meta.fields
+ exclude = cls._meta.exclude
+
+ assert not (fields is None and exclude is None), \
+ "Setting 'Meta.model' without either 'Meta.fields' or 'Meta.exclude' " \
+ "has been deprecated since 0.15.0 and is now disallowed. Add an explicit " \
+ "'Meta.fields' or 'Meta.exclude' to the %s class." % cls.__name__
+
+ # Setting exclude with no fields implies all other fields.
+ if exclude is not None and fields is None:
+ fields = ALL_FIELDS
+
+ # Resolve ALL_FIELDS into all fields for the filterset's model.
+ if fields == ALL_FIELDS:
+ fields = get_all_model_fields(model)
+
+ # Remove excluded fields
+ exclude = exclude or []
+ if not isinstance(fields, dict):
+ fields = [(f, [settings.DEFAULT_LOOKUP_EXPR]) for f in fields if f not in exclude]
+ else:
+ fields = [(f, lookups) for f, lookups in fields.items() if f not in exclude]
+
+ return OrderedDict(fields)
+
+ @classmethod
+ def get_filter_name(cls, field_name, lookup_expr):
+ """
+ Combine a field name and lookup expression into a usable filter name.
+ Exact lookups are the implicit default, so "exact" is stripped from the
+ end of the filter name.
+ """
+ filter_name = LOOKUP_SEP.join([field_name, lookup_expr])
+
+ # This also works with transformed exact lookups, such as 'date__exact'
+ _default_expr = LOOKUP_SEP + settings.DEFAULT_LOOKUP_EXPR
+ if filter_name.endswith(_default_expr):
+ filter_name = filter_name[:-len(_default_expr)]
+
+ return filter_name
+
+ @classmethod
+ def get_filters(cls):
+ """
+ Get all filters for the filterset. This is the combination of declared and
+ generated filters.
+ """
+
+ # No model specified - skip filter generation
+ if not cls._meta.model:
+ return cls.declared_filters.copy()
+
+ # Determine the filters that should be included on the filterset.
+ filters = OrderedDict()
+ fields = cls.get_fields()
+ undefined = []
+
+ for field_name, lookups in fields.items():
+ field = get_model_field(cls._meta.model, field_name)
+
+ # warn if the field doesn't exist.
+ if field is None:
+ undefined.append(field_name)
+
+ for lookup_expr in lookups:
+ filter_name = cls.get_filter_name(field_name, lookup_expr)
+
+ # If the filter is explicitly declared on the class, skip generation
+ if filter_name in cls.declared_filters:
+ filters[filter_name] = cls.declared_filters[filter_name]
+ continue
+
+ if field is not None:
+ filters[filter_name] = cls.filter_for_field(field, field_name, lookup_expr)
+
+ # Allow Meta.fields to contain declared filters *only* when a list/tuple
+ if isinstance(cls._meta.fields, (list, tuple)):
+ undefined = [f for f in undefined if f not in cls.declared_filters]
+
+ if undefined:
+ raise TypeError(
+ "'Meta.fields' must not contain non-model field names: %s"
+ % ', '.join(undefined)
+ )
+
+ # Add in declared filters. This is necessary since we don't enforce adding
+ # declared filters to the 'Meta.fields' option
+ filters.update(cls.declared_filters)
+ return filters
+
+ @classmethod
+ def filter_for_field(cls, field, field_name, lookup_expr=None):
+ if lookup_expr is None:
+ lookup_expr = settings.DEFAULT_LOOKUP_EXPR
+ field, lookup_type = resolve_field(field, lookup_expr)
+
+ default = {
+ 'field_name': field_name,
+ 'lookup_expr': lookup_expr,
+ }
+
+ filter_class, params = cls.filter_for_lookup(field, lookup_type)
+ default.update(params)
+
+ assert filter_class is not None, (
+ "%s resolved field '%s' with '%s' lookup to an unrecognized field "
+ "type %s. Try adding an override to 'Meta.filter_overrides'. See: "
+ "https://django-filter.readthedocs.io/en/main/ref/filterset.html"
+ "#customise-filter-generation-with-filter-overrides"
+ ) % (cls.__name__, field_name, lookup_expr, field.__class__.__name__)
+
+ return filter_class(**default)
+
+ @classmethod
+ def filter_for_lookup(cls, field, lookup_type):
+ DEFAULTS = dict(cls.FILTER_DEFAULTS)
+ if hasattr(cls, '_meta'):
+ DEFAULTS.update(cls._meta.filter_overrides)
+
+ data = try_dbfield(DEFAULTS.get, field.__class__) or {}
+ filter_class = data.get('filter_class')
+ params = data.get('extra', lambda field: {})(field)
+
+ # if there is no filter class, exit early
+ if not filter_class:
+ return None, {}
+
+ # perform lookup specific checks
+ if lookup_type == 'exact' and getattr(field, 'choices', None):
+ return ChoiceFilter, {'choices': field.choices}
+
+ if lookup_type == 'isnull':
+ data = try_dbfield(DEFAULTS.get, models.BooleanField)
+
+ filter_class = data.get('filter_class')
+ params = data.get('extra', lambda field: {})(field)
+ return filter_class, params
+
+ if lookup_type == 'in':
+ class ConcreteInFilter(BaseInFilter, filter_class):
+ pass
+ ConcreteInFilter.__name__ = cls._csv_filter_class_name(
+ filter_class, lookup_type
+ )
+
+ return ConcreteInFilter, params
+
+ if lookup_type == 'range':
+ class ConcreteRangeFilter(BaseRangeFilter, filter_class):
+ pass
+ ConcreteRangeFilter.__name__ = cls._csv_filter_class_name(
+ filter_class, lookup_type
+ )
+
+ return ConcreteRangeFilter, params
+
+ return filter_class, params
+
+ @classmethod
+ def _csv_filter_class_name(cls, filter_class, lookup_type):
+ """
+ Generate a suitable class name for a concrete filter class. This is not
+ completely reliable, as not all filter class names are of the format
+ Filter.
+
+ ex::
+
+ FilterSet._csv_filter_class_name(DateTimeFilter, 'in')
+
+ returns 'DateTimeInFilter'
+
+ """
+ # DateTimeFilter => DateTime
+ type_name = filter_class.__name__
+ if type_name.endswith('Filter'):
+ type_name = type_name[:-6]
+
+ # in => In
+ lookup_name = lookup_type.capitalize()
+
+ # DateTimeInFilter
+ return str('%s%sFilter' % (type_name, lookup_name))
+
+
+class FilterSet(BaseFilterSet, metaclass=FilterSetMetaclass):
+ pass
+
+
+def filterset_factory(model, fields=ALL_FIELDS):
+ meta = type(str('Meta'), (object,), {'model': model, 'fields': fields})
+ filterset = type(str('%sFilterSet' % model._meta.object_name),
+ (FilterSet,), {'Meta': meta})
+ return filterset
diff --git a/venv/Lib/site-packages/django_filters/locale/ar/LC_MESSAGES/django.mo b/venv/Lib/site-packages/django_filters/locale/ar/LC_MESSAGES/django.mo
new file mode 100644
index 0000000..b1b876f
Binary files /dev/null and b/venv/Lib/site-packages/django_filters/locale/ar/LC_MESSAGES/django.mo differ
diff --git a/venv/Lib/site-packages/django_filters/locale/ar/LC_MESSAGES/django.po b/venv/Lib/site-packages/django_filters/locale/ar/LC_MESSAGES/django.po
new file mode 100644
index 0000000..d35fe14
--- /dev/null
+++ b/venv/Lib/site-packages/django_filters/locale/ar/LC_MESSAGES/django.po
@@ -0,0 +1,189 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+# FIRST AUTHOR , YEAR.
+# FULL NAME , 2020.
+#
+#: conf.py:29 conf.py:30 conf.py:43
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE VERSION\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2020-03-24 00:19+0100\n"
+"PO-Revision-Date: 2020-03-24 00:48+0100\n"
+"Last-Translator: FULL NAME \n"
+"Language-Team: LANGUAGE \n"
+"Language: \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=6; plural=n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 "
+"&& n%100<=10 ? 3 : n%100>=11 && n%100<=99 ? 4 : 5;\n"
+"X-Generator: Gtranslator 2.91.7\n"
+
+#: conf.py:19
+msgid "date"
+msgstr "تاريخ"
+
+#: conf.py:20
+msgid "year"
+msgstr "سنة"
+
+#: conf.py:21
+msgid "month"
+msgstr "شهر"
+
+#: conf.py:22
+msgid "day"
+msgstr "يوم"
+
+#: conf.py:23
+msgid "week day"
+msgstr "يوم الأسبوع"
+
+#: conf.py:24
+msgid "hour"
+msgstr "ساعة"
+
+#: conf.py:25
+msgid "minute"
+msgstr "دقيقة"
+
+#: conf.py:26
+msgid "second"
+msgstr "ثانية"
+
+#: conf.py:31 conf.py:32
+msgid "contains"
+msgstr "يحتوي على"
+
+#: conf.py:33
+msgid "is in"
+msgstr "في داخل"
+
+#: conf.py:34
+msgid "is greater than"
+msgstr "أكبر من"
+
+#: conf.py:35
+msgid "is greater than or equal to"
+msgstr "أكبر من أو يساوي"
+
+#: conf.py:36
+msgid "is less than"
+msgstr "أصغر من"
+
+#: conf.py:37
+msgid "is less than or equal to"
+msgstr "أصغر من أو يساوي"
+
+#: conf.py:38 conf.py:39
+msgid "starts with"
+msgstr "يبدأ ب"
+
+#: conf.py:40 conf.py:41
+msgid "ends with"
+msgstr "ينتهي ب"
+
+#: conf.py:42
+msgid "is in range"
+msgstr "في النطاق"
+
+#: conf.py:44 conf.py:45
+msgid "matches regex"
+msgstr "يطابق التعبير العادي"
+
+#: conf.py:46 conf.py:54
+msgid "search"
+msgstr "بحث"
+
+#: conf.py:49
+msgid "is contained by"
+msgstr "موجود في"
+
+#: conf.py:50
+msgid "overlaps"
+msgstr "يتداخل"
+
+#: conf.py:51
+msgid "has key"
+msgstr "لديه مفتاح"
+
+#: conf.py:52
+msgid "has keys"
+msgstr "لديه مفاتيح"
+
+#: conf.py:53
+msgid "has any keys"
+msgstr "لديه أي مفاتيح"
+
+#: fields.py:106
+msgid "Select a lookup."
+msgstr "حدد بحث"
+
+#: fields.py:198
+msgid "Range query expects two values."
+msgstr "إستعلام النطاق يتوقع قيمتين"
+
+#: filters.py:402
+msgid "Today"
+msgstr "اليوم"
+
+#: filters.py:403
+msgid "Yesterday"
+msgstr "أمس"
+
+#: filters.py:404
+msgid "Past 7 days"
+msgstr "الأيام السبعة الماضية"
+
+#: filters.py:405
+msgid "This month"
+msgstr "هذا الشهر"
+
+#: filters.py:406
+msgid "This year"
+msgstr "هذه السنة"
+
+#: filters.py:504
+msgid "Multiple values may be separated by commas."
+msgstr "يمكن فصل القيم المتعددة بفواصل."
+
+#: filters.py:677
+#, python-format
+msgid "%s (descending)"
+msgstr "%s (تنازلي)"
+
+#: filters.py:693
+msgid "Ordering"
+msgstr "الترتيب"
+
+#: rest_framework/filterset.py:31
+#: templates/django_filters/rest_framework/form.html:5
+msgid "Submit"
+msgstr "إرسال"
+
+#: templates/django_filters/rest_framework/crispy_form.html:4
+#: templates/django_filters/rest_framework/form.html:2
+msgid "Field filters"
+msgstr "مرشحات الحقل"
+
+#: utils.py:294
+msgid "exclude"
+msgstr "استبعاد"
+
+#: widgets.py:58
+msgid "All"
+msgstr "كل"
+
+#: widgets.py:160
+msgid "Unknown"
+msgstr "مجهول"
+
+#: widgets.py:161
+msgid "Yes"
+msgstr "نعم"
+
+#: widgets.py:162
+msgid "No"
+msgstr "لا"
diff --git a/venv/Lib/site-packages/django_filters/locale/be/LC_MESSAGES/django.mo b/venv/Lib/site-packages/django_filters/locale/be/LC_MESSAGES/django.mo
new file mode 100644
index 0000000..595dad9
Binary files /dev/null and b/venv/Lib/site-packages/django_filters/locale/be/LC_MESSAGES/django.mo differ
diff --git a/venv/Lib/site-packages/django_filters/locale/be/LC_MESSAGES/django.po b/venv/Lib/site-packages/django_filters/locale/be/LC_MESSAGES/django.po
new file mode 100644
index 0000000..ebd0221
--- /dev/null
+++ b/venv/Lib/site-packages/django_filters/locale/be/LC_MESSAGES/django.po
@@ -0,0 +1,185 @@
+#
+#: conf.py:27 conf.py:28 conf.py:41
+msgid ""
+msgstr ""
+"Project-Id-Version: django-filter\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2018-01-24 18:51+0500\n"
+"PO-Revision-Date: 2016-09-29 11:47+0300\n"
+"Last-Translator: Eugena Mikhaylikova \n"
+"Language-Team: TextTempearture\n"
+"Language: be\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=4; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n"
+"%10<=4 && (n%100<12 || n%100>14) ? 1 : n%10==0 || (n%10>=5 && n%10<=9) || (n"
+"%100>=11 && n%100<=14)? 2 : 3);\n"
+"X-Generator: Poedit 1.8.9\n"
+
+#: conf.py:17
+msgid "date"
+msgstr "дата"
+
+#: conf.py:18
+msgid "year"
+msgstr "год"
+
+#: conf.py:19
+msgid "month"
+msgstr "месяц"
+
+#: conf.py:20
+msgid "day"
+msgstr "дзень"
+
+#: conf.py:21
+msgid "week day"
+msgstr "дзень тыдня"
+
+#: conf.py:22
+msgid "hour"
+msgstr "гадзіну"
+
+#: conf.py:23
+msgid "minute"
+msgstr "хвіліна"
+
+#: conf.py:24
+msgid "second"
+msgstr "секунда"
+
+#: conf.py:29 conf.py:30
+msgid "contains"
+msgstr "змяшчае"
+
+#: conf.py:31
+msgid "is in"
+msgstr "у"
+
+#: conf.py:32
+msgid "is greater than"
+msgstr "больш чым"
+
+#: conf.py:33
+msgid "is greater than or equal to"
+msgstr "больш або роўна"
+
+#: conf.py:34
+msgid "is less than"
+msgstr "менш чым"
+
+#: conf.py:35
+msgid "is less than or equal to"
+msgstr "менш або роўна"
+
+#: conf.py:36 conf.py:37
+msgid "starts with"
+msgstr "пачынаецца"
+
+#: conf.py:38 conf.py:39
+msgid "ends with"
+msgstr "заканчваецца"
+
+#: conf.py:40
+msgid "is in range"
+msgstr "у дыяпазоне"
+
+#: conf.py:42 conf.py:43
+msgid "matches regex"
+msgstr "адпавядае рэгулярнаму выразу"
+
+#: conf.py:44 conf.py:52
+msgid "search"
+msgstr "пошук"
+
+#: conf.py:47
+msgid "is contained by"
+msgstr "змяшчаецца ў"
+
+#: conf.py:48
+msgid "overlaps"
+msgstr "перакрываецца"
+
+#: conf.py:49
+msgid "has key"
+msgstr "мае ключ"
+
+#: conf.py:50
+msgid "has keys"
+msgstr "мае ключы"
+
+#: conf.py:51
+msgid "has any keys"
+msgstr "мае любыя ключы"
+
+#: fields.py:178
+msgid "Range query expects two values."
+msgstr "Запыт дыяпазону чакае два значэння."
+
+#: filters.py:429
+msgid "Any date"
+msgstr "Любая дата"
+
+#: filters.py:430
+msgid "Today"
+msgstr "Сёння"
+
+#: filters.py:435
+msgid "Past 7 days"
+msgstr "Мінулыя 7 дзён"
+
+#: filters.py:439
+msgid "This month"
+msgstr "За гэты месяц"
+
+#: filters.py:443
+msgid "This year"
+msgstr "У гэтым годзе"
+
+#: filters.py:446
+msgid "Yesterday"
+msgstr "Учора"
+
+#: filters.py:512
+msgid "Multiple values may be separated by commas."
+msgstr "Некалькі значэнняў могуць быць падзеленыя коскамі."
+
+#: filters.py:591
+#, python-format
+msgid "%s (descending)"
+msgstr "%s (па змяншэнні)"
+
+#: filters.py:607
+msgid "Ordering"
+msgstr "Парадак"
+
+#: rest_framework/filterset.py:30
+#: templates/django_filters/rest_framework/form.html:5
+msgid "Submit"
+msgstr "Адправіць"
+
+#: templates/django_filters/rest_framework/crispy_form.html:4
+#: templates/django_filters/rest_framework/form.html:2
+msgid "Field filters"
+msgstr "Фільтры па палях"
+
+#: utils.py:224
+msgid "exclude"
+msgstr "выключаючы"
+
+#: widgets.py:57
+msgid "All"
+msgstr "Усе"
+
+#: widgets.py:159
+msgid "Unknown"
+msgstr "Не было прапанавана"
+
+#: widgets.py:160
+msgid "Yes"
+msgstr "Ды"
+
+#: widgets.py:161
+msgid "No"
+msgstr "Няма"
diff --git a/venv/Lib/site-packages/django_filters/locale/bg/LC_MESSAGES/django.mo b/venv/Lib/site-packages/django_filters/locale/bg/LC_MESSAGES/django.mo
new file mode 100644
index 0000000..122fe4d
Binary files /dev/null and b/venv/Lib/site-packages/django_filters/locale/bg/LC_MESSAGES/django.mo differ
diff --git a/venv/Lib/site-packages/django_filters/locale/bg/LC_MESSAGES/django.po b/venv/Lib/site-packages/django_filters/locale/bg/LC_MESSAGES/django.po
new file mode 100644
index 0000000..9149eaa
--- /dev/null
+++ b/venv/Lib/site-packages/django_filters/locale/bg/LC_MESSAGES/django.po
@@ -0,0 +1,187 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+# Hristo Gatsinski , 2019.
+#
+#: conf.py:27 conf.py:28 conf.py:41
+msgid ""
+msgstr ""
+"Project-Id-Version: django-filter\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2019-12-22 19:45+0200\n"
+"PO-Revision-Date: 2019-12-21 19:36+0200\n"
+"Last-Translator: Hristo Gatsinski \n"
+"Language-Team: \n"
+"Language: bg\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+"X-Generator: Poedit 1.8.9\n"
+
+#: conf.py:17
+msgid "date"
+msgstr "дата"
+
+#: conf.py:18
+msgid "year"
+msgstr "година"
+
+#: conf.py:19
+msgid "month"
+msgstr "месец"
+
+#: conf.py:20
+msgid "day"
+msgstr "ден"
+
+#: conf.py:21
+msgid "week day"
+msgstr "ден от седмицата"
+
+#: conf.py:22
+msgid "hour"
+msgstr "час"
+
+#: conf.py:23
+msgid "minute"
+msgstr "минута"
+
+#: conf.py:24
+msgid "second"
+msgstr "секунда"
+
+#: conf.py:29 conf.py:30
+msgid "contains"
+msgstr "съдържа"
+
+#: conf.py:31
+msgid "is in"
+msgstr "в"
+
+#: conf.py:32
+msgid "is greater than"
+msgstr "е по-голям от"
+
+#: conf.py:33
+msgid "is greater than or equal to"
+msgstr "е по-голям или равен на"
+
+#: conf.py:34
+msgid "is less than"
+msgstr "е по-малък от"
+
+#: conf.py:35
+msgid "is less than or equal to"
+msgstr "е по-малък или равен на"
+
+#: conf.py:36 conf.py:37
+msgid "starts with"
+msgstr "започва с"
+
+#: conf.py:38 conf.py:39
+msgid "ends with"
+msgstr "завършва с"
+
+#: conf.py:40
+msgid "is in range"
+msgstr "е в диапазона"
+
+#: conf.py:42 conf.py:43
+msgid "matches regex"
+msgstr "съвпада с регуларен израз"
+
+#: conf.py:44 conf.py:52
+msgid "search"
+msgstr "търсене"
+
+#: conf.py:47
+msgid "is contained by"
+msgstr "се съдържа от"
+
+#: conf.py:48
+msgid "overlaps"
+msgstr "припокрива"
+
+#: conf.py:49
+msgid "has key"
+msgstr "има ключ"
+
+#: conf.py:50
+msgid "has keys"
+msgstr "има ключове"
+
+#: conf.py:51
+msgid "has any keys"
+msgstr "има който и да е ключ"
+
+#: fields.py:106
+msgid "Select a lookup."
+msgstr "Изберете справка"
+
+#: fields.py:198
+msgid "Range query expects two values."
+msgstr "Търсенето по диапазон изисква две стойности"
+
+#: filters.py:406
+msgid "Today"
+msgstr "Днес"
+
+#: filters.py:407
+msgid "Yesterday"
+msgstr "Вчера"
+
+#: filters.py:408
+msgid "Past 7 days"
+msgstr "Последните 7 дни"
+
+#: filters.py:409
+msgid "This month"
+msgstr "Този месец"
+
+#: filters.py:410
+msgid "This year"
+msgstr "Тази година"
+
+#: filters.py:508
+msgid "Multiple values may be separated by commas."
+msgstr "Множество стойности може да се разделят със запетая"
+
+#: filters.py:681
+#, python-format
+msgid "%s (descending)"
+msgstr "%s (намалавящ)"
+
+#: filters.py:697
+msgid "Ordering"
+msgstr "Подредба"
+
+#: rest_framework/filterset.py:31
+#: templates/django_filters/rest_framework/form.html:5
+msgid "Submit"
+msgstr "Изпращане"
+
+#: templates/django_filters/rest_framework/crispy_form.html:4
+#: templates/django_filters/rest_framework/form.html:2
+msgid "Field filters"
+msgstr "Филтри на полетата"
+
+#: utils.py:298
+msgid "exclude"
+msgstr "изключва"
+
+#: widgets.py:57
+msgid "All"
+msgstr "Всичко"
+
+#: widgets.py:159
+msgid "Unknown"
+msgstr "Неизвестен"
+
+#: widgets.py:160
+msgid "Yes"
+msgstr "Да"
+
+#: widgets.py:161
+msgid "No"
+msgstr "Не"
diff --git a/venv/Lib/site-packages/django_filters/locale/cs/LC_MESSAGES/django.mo b/venv/Lib/site-packages/django_filters/locale/cs/LC_MESSAGES/django.mo
new file mode 100644
index 0000000..54a8915
Binary files /dev/null and b/venv/Lib/site-packages/django_filters/locale/cs/LC_MESSAGES/django.mo differ
diff --git a/venv/Lib/site-packages/django_filters/locale/cs/LC_MESSAGES/django.po b/venv/Lib/site-packages/django_filters/locale/cs/LC_MESSAGES/django.po
new file mode 100644
index 0000000..40930a6
--- /dev/null
+++ b/venv/Lib/site-packages/django_filters/locale/cs/LC_MESSAGES/django.po
@@ -0,0 +1,182 @@
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: django-filter\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2018-01-24 11:03+0500\n"
+"PO-Revision-Date: 2016-09-29 11:47+0300\n"
+"Last-Translator: Eugena Mikhaylikova \n"
+"Language-Team: TextTempearture\n"
+"Language: cs\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=4; plural=(n == 1 && n % 1 == 0) ? 0 : (n >= 2 && n "
+"<= 4 && n % 1 == 0) ? 1: (n % 1 != 0 ) ? 2 : 3;\n"
+"X-Generator: Poedit 1.8.9\n"
+
+#: conf.py:17
+msgid "date"
+msgstr "datum"
+
+#: conf.py:18
+msgid "year"
+msgstr "rok"
+
+#: conf.py:19
+msgid "month"
+msgstr "měsíc"
+
+#: conf.py:20
+msgid "day"
+msgstr "den"
+
+#: conf.py:21
+msgid "week day"
+msgstr "den v týdnu"
+
+#: conf.py:22
+msgid "hour"
+msgstr "hodinu"
+
+#: conf.py:23
+msgid "minute"
+msgstr "minutu"
+
+#: conf.py:24
+msgid "second"
+msgstr "vteřina"
+
+#: conf.py:29 conf.py:30
+msgid "contains"
+msgstr "obsahuje"
+
+#: conf.py:31
+msgid "is in"
+msgstr "v"
+
+#: conf.py:32
+msgid "is greater than"
+msgstr "více než"
+
+#: conf.py:33
+msgid "is greater than or equal to"
+msgstr "větší nebo roven"
+
+#: conf.py:34
+msgid "is less than"
+msgstr "méně než"
+
+#: conf.py:35
+msgid "is less than or equal to"
+msgstr "menší nebo rovné"
+
+#: conf.py:36 conf.py:37
+msgid "starts with"
+msgstr "začíná"
+
+#: conf.py:38 conf.py:39
+msgid "ends with"
+msgstr "končí"
+
+#: conf.py:40
+msgid "is in range"
+msgstr "v rozsahu"
+
+#: conf.py:42 conf.py:43
+msgid "matches regex"
+msgstr "odpovídá normálnímu výrazu"
+
+#: conf.py:44 conf.py:52
+msgid "search"
+msgstr "vyhledávání"
+
+#: conf.py:47
+msgid "is contained by"
+msgstr "je obsažen v"
+
+#: conf.py:48
+msgid "overlaps"
+msgstr "překrývají"
+
+#: conf.py:49
+msgid "has key"
+msgstr "má klíč"
+
+#: conf.py:50
+msgid "has keys"
+msgstr "má klíče"
+
+#: conf.py:51
+msgid "has any keys"
+msgstr "má nějaké klíče"
+
+#: fields.py:178
+msgid "Range query expects two values."
+msgstr "Rozsah dotazu očekává dvě hodnoty."
+
+#: filters.py:429
+msgid "Any date"
+msgstr "Jakékoliv datum"
+
+#: filters.py:430
+msgid "Today"
+msgstr "Dnes"
+
+#: filters.py:435
+msgid "Past 7 days"
+msgstr "Posledních 7 dní"
+
+#: filters.py:439
+msgid "This month"
+msgstr "Tento měsíc"
+
+#: filters.py:443
+msgid "This year"
+msgstr "Tento rok"
+
+#: filters.py:446
+msgid "Yesterday"
+msgstr "Včera"
+
+#: filters.py:512
+msgid "Multiple values may be separated by commas."
+msgstr "Více hodnot lze oddělit čárkami."
+
+#: filters.py:591
+msgid "%s (descending)"
+msgstr "%s (sestupně)"
+
+#: filters.py:607
+msgid "Ordering"
+msgstr "Řád z"
+
+#: rest_framework/filterset.py:30
+#: templates/django_filters/rest_framework/form.html:5
+msgid "Submit"
+msgstr "Odeslat"
+
+#: templates/django_filters/rest_framework/crispy_form.html:4
+#: templates/django_filters/rest_framework/form.html:2
+msgid "Field filters"
+msgstr "Filtry na polích"
+
+#: utils.py:224
+msgid "exclude"
+msgstr "s výjimkou"
+
+#: widgets.py:57
+msgid "All"
+msgstr "Všechno"
+
+#: widgets.py:159
+msgid "Unknown"
+msgstr "Není nastaveno"
+
+#: widgets.py:160
+msgid "Yes"
+msgstr "Ano"
+
+#: widgets.py:161
+msgid "No"
+msgstr "Ne"
diff --git a/venv/Lib/site-packages/django_filters/locale/da/LC_MESSAGES/django.mo b/venv/Lib/site-packages/django_filters/locale/da/LC_MESSAGES/django.mo
new file mode 100644
index 0000000..84f7e94
Binary files /dev/null and b/venv/Lib/site-packages/django_filters/locale/da/LC_MESSAGES/django.mo differ
diff --git a/venv/Lib/site-packages/django_filters/locale/da/LC_MESSAGES/django.po b/venv/Lib/site-packages/django_filters/locale/da/LC_MESSAGES/django.po
new file mode 100644
index 0000000..6260b3f
--- /dev/null
+++ b/venv/Lib/site-packages/django_filters/locale/da/LC_MESSAGES/django.po
@@ -0,0 +1,181 @@
+msgid ""
+msgstr ""
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"X-Generator: Poedit 2.0.1\n"
+"Project-Id-Version: django-filter\n"
+"Language: da\n"
+"Last-Translator: Danni Randeris \n"
+"Language-Team: Danni Randeris \n"
+"POT-Creation-Date: 2017-10-28\n"
+"PO-Revision-Date: 2017-10-28\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: conf.py:17
+msgid "date"
+msgstr "dato"
+
+#: conf.py:18
+msgid "year"
+msgstr "år"
+
+#: conf.py:19
+msgid "month"
+msgstr "måned"
+
+#: conf.py:20
+msgid "day"
+msgstr "dag"
+
+#: conf.py:21
+msgid "week day"
+msgstr "ugedag"
+
+#: conf.py:22
+msgid "hour"
+msgstr "time"
+
+#: conf.py:23
+msgid "minute"
+msgstr "minut"
+
+#: conf.py:24
+msgid "second"
+msgstr "sekund"
+
+#: conf.py:29 conf.py:30
+msgid "contains"
+msgstr "indeholder"
+
+#: conf.py:31
+msgid "is in"
+msgstr "er i"
+
+#: conf.py:32
+msgid "is greater than"
+msgstr "er større end"
+
+#: conf.py:33
+msgid "is greater than or equal to"
+msgstr "er større end eller lig med"
+
+#: conf.py:34
+msgid "is less than"
+msgstr "er mindre end"
+
+#: conf.py:35
+msgid "is less than or equal to"
+msgstr "er mindre end eller lig med"
+
+#: conf.py:36 conf.py:37
+msgid "starts with"
+msgstr "starter med"
+
+#: conf.py:38 conf.py:39
+msgid "ends with"
+msgstr "slutter med"
+
+#: conf.py:40
+msgid "is in range"
+msgstr "er i intervallet"
+
+#: conf.py:42 conf.py:43
+msgid "matches regex"
+msgstr "matcher regex"
+
+#: conf.py:44 conf.py:52
+msgid "search"
+msgstr "søg"
+
+#: conf.py:47
+msgid "is contained by"
+msgstr "er indeholdt af"
+
+#: conf.py:48
+msgid "overlaps"
+msgstr "overlapper"
+
+#: conf.py:49
+msgid "has key"
+msgstr "har string"
+
+#: conf.py:50
+msgid "has keys"
+msgstr "har stringe"
+
+#: conf.py:51
+msgid "has any keys"
+msgstr "har hvilken som helst string"
+
+#: fields.py:178
+msgid "Range query expects two values."
+msgstr "Interval forespørgslen forventer to værdier."
+
+#: filters.py:429
+msgid "Any date"
+msgstr "Hvilken som helst dag"
+
+#: filters.py:430
+msgid "Today"
+msgstr "I dag"
+
+#: filters.py:435
+msgid "Past 7 days"
+msgstr "Sidste 7 dage"
+
+#: filters.py:439
+msgid "This month"
+msgstr "Denne måned"
+
+#: filters.py:443
+msgid "This year"
+msgstr "Dette år"
+
+#: filters.py:446
+msgid "Yesterday"
+msgstr "I går"
+
+#: filters.py:512
+msgid "Multiple values may be separated by commas."
+msgstr "Flere værdier kan adskilles via komma."
+
+#: filters.py:591
+msgid "%s (descending)"
+msgstr "%s (aftagende)"
+
+#: filters.py:607
+msgid "Ordering"
+msgstr "Sortering"
+
+#: rest_framework/filterset.py:30
+#: templates/django_filters/rest_framework/form.html:5
+#, fuzzy
+msgid "Submit"
+msgstr "Indsend"
+
+#: templates/django_filters/rest_framework/crispy_form.html:4
+#: templates/django_filters/rest_framework/form.html:2
+#, fuzzy
+msgid "Field filters"
+msgstr "Felt filtre"
+
+#: utils.py:224
+msgid "exclude"
+msgstr "udelad"
+
+#: widgets.py:57
+msgid "All"
+msgstr "Alle"
+
+#: widgets.py:159
+msgid "Unknown"
+msgstr "Ukendt"
+
+#: widgets.py:160
+msgid "Yes"
+msgstr "Ja"
+
+#: widgets.py:161
+msgid "No"
+msgstr "Nej"
diff --git a/venv/Lib/site-packages/django_filters/locale/de/LC_MESSAGES/django.mo b/venv/Lib/site-packages/django_filters/locale/de/LC_MESSAGES/django.mo
new file mode 100644
index 0000000..9a48a0a
Binary files /dev/null and b/venv/Lib/site-packages/django_filters/locale/de/LC_MESSAGES/django.mo differ
diff --git a/venv/Lib/site-packages/django_filters/locale/de/LC_MESSAGES/django.po b/venv/Lib/site-packages/django_filters/locale/de/LC_MESSAGES/django.po
new file mode 100644
index 0000000..428a2ff
--- /dev/null
+++ b/venv/Lib/site-packages/django_filters/locale/de/LC_MESSAGES/django.po
@@ -0,0 +1,187 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+# FIRST AUTHOR , YEAR.
+#
+#: conf.py:27 conf.py:28 conf.py:41
+msgid ""
+msgstr ""
+"Project-Id-Version: django-filter\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2018-01-24 11:03+0500\n"
+"PO-Revision-Date: 2013-08-10 12:29+0100\n"
+"Last-Translator: Florian Apolloner \n"
+"Language-Team: \n"
+"Language: de\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+"X-Generator: Poedit 1.5.4\n"
+
+#: conf.py:17
+msgid "date"
+msgstr "Datum"
+
+#: conf.py:18
+msgid "year"
+msgstr "Jahr"
+
+#: conf.py:19
+msgid "month"
+msgstr "Monat"
+
+#: conf.py:20
+msgid "day"
+msgstr "Tag"
+
+#: conf.py:21
+msgid "week day"
+msgstr "Wochentag"
+
+#: conf.py:22
+msgid "hour"
+msgstr "Stunde"
+
+#: conf.py:23
+msgid "minute"
+msgstr "Minute"
+
+#: conf.py:24
+msgid "second"
+msgstr "Sekunde"
+
+#: conf.py:29 conf.py:30
+msgid "contains"
+msgstr "enthält"
+
+#: conf.py:31
+msgid "is in"
+msgstr "ist in"
+
+#: conf.py:32
+msgid "is greater than"
+msgstr "ist größer als"
+
+#: conf.py:33
+msgid "is greater than or equal to"
+msgstr "ist größer oder gleich"
+
+#: conf.py:34
+msgid "is less than"
+msgstr "ist kleiner als"
+
+#: conf.py:35
+msgid "is less than or equal to"
+msgstr "ist kleiner oder gleich"
+
+#: conf.py:36 conf.py:37
+msgid "starts with"
+msgstr "beginnt mit"
+
+#: conf.py:38 conf.py:39
+msgid "ends with"
+msgstr "endet mit"
+
+#: conf.py:40
+msgid "is in range"
+msgstr "ist im Bereich"
+
+#: conf.py:42 conf.py:43
+msgid "matches regex"
+msgstr "passt auf Regex"
+
+#: conf.py:44 conf.py:52
+msgid "search"
+msgstr "Suche"
+
+#: conf.py:47
+msgid "is contained by"
+msgstr "ist enthalten in"
+
+#: conf.py:48
+msgid "overlaps"
+msgstr "überlappen"
+
+#: conf.py:49
+msgid "has key"
+msgstr "hat Schlüssel"
+
+#: conf.py:50
+msgid "has keys"
+msgstr "hat Schlüssel"
+
+#: conf.py:51
+msgid "has any keys"
+msgstr "hat beliebige Schlüssel"
+
+#: fields.py:178
+msgid "Range query expects two values."
+msgstr "Die Bereichsabfrage erwartet zwei Werte."
+
+#: filters.py:429
+msgid "Any date"
+msgstr "Alle Daten"
+
+#: filters.py:430
+msgid "Today"
+msgstr "Heute"
+
+#: filters.py:435
+msgid "Past 7 days"
+msgstr "Letzte 7 Tage"
+
+#: filters.py:439
+msgid "This month"
+msgstr "Diesen Monat"
+
+#: filters.py:443
+msgid "This year"
+msgstr "Dieses Jahr"
+
+#: filters.py:446
+msgid "Yesterday"
+msgstr "Gestern"
+
+#: filters.py:512
+msgid "Multiple values may be separated by commas."
+msgstr "Mehrere Werte können durch Kommas getrennt sein."
+
+#: filters.py:591
+#, python-format
+msgid "%s (descending)"
+msgstr "%s (absteigend)"
+
+#: filters.py:607
+msgid "Ordering"
+msgstr "Sortierung"
+
+#: rest_framework/filterset.py:30
+#: templates/django_filters/rest_framework/form.html:5
+msgid "Submit"
+msgstr "Absenden"
+
+#: templates/django_filters/rest_framework/crispy_form.html:4
+#: templates/django_filters/rest_framework/form.html:2
+msgid "Field filters"
+msgstr "Feldfilter"
+
+#: utils.py:224
+msgid "exclude"
+msgstr "ausschließen"
+
+#: widgets.py:57
+msgid "All"
+msgstr "Alle"
+
+#: widgets.py:159
+msgid "Unknown"
+msgstr "Unbekannte"
+
+#: widgets.py:160
+msgid "Yes"
+msgstr "Ja"
+
+#: widgets.py:161
+msgid "No"
+msgstr "Nein"
diff --git a/venv/Lib/site-packages/django_filters/locale/el/LC_MESSAGES/django.mo b/venv/Lib/site-packages/django_filters/locale/el/LC_MESSAGES/django.mo
new file mode 100644
index 0000000..4e2258b
Binary files /dev/null and b/venv/Lib/site-packages/django_filters/locale/el/LC_MESSAGES/django.mo differ
diff --git a/venv/Lib/site-packages/django_filters/locale/el/LC_MESSAGES/django.po b/venv/Lib/site-packages/django_filters/locale/el/LC_MESSAGES/django.po
new file mode 100644
index 0000000..0018a2c
--- /dev/null
+++ b/venv/Lib/site-packages/django_filters/locale/el/LC_MESSAGES/django.po
@@ -0,0 +1,187 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+# Serafeim Papastefanos , 2017.
+#
+#: .\conf.py:27 .\conf.py:28 .\conf.py:41
+msgid ""
+msgstr ""
+"Project-Id-Version: django-filter\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2017-11-16 10:06+0200\n"
+"PO-Revision-Date: 2017-11-16 10:04+0200\n"
+"Last-Translator: Serafeim Papastefanos \n"
+"Language-Team: \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: de\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+"X-Generator: Poedit 1.6.5\n"
+
+#: .\conf.py:17
+msgid "date"
+msgstr "ημερομηνία"
+
+#: .\conf.py:18
+msgid "year"
+msgstr "έτος"
+
+#: .\conf.py:19
+msgid "month"
+msgstr "μήνας"
+
+#: .\conf.py:20
+msgid "day"
+msgstr "ημέρα"
+
+#: .\conf.py:21
+msgid "week day"
+msgstr "ημέρα της εβδομάδας"
+
+#: .\conf.py:22
+msgid "hour"
+msgstr "ώρα"
+
+#: .\conf.py:23
+msgid "minute"
+msgstr "λεπτό"
+
+#: .\conf.py:24
+msgid "second"
+msgstr "δευτερόλεπτο"
+
+#: .\conf.py:29 .\conf.py:30
+msgid "contains"
+msgstr "περιέχει"
+
+#: .\conf.py:31
+msgid "is in"
+msgstr "είναι εντός των"
+
+#: .\conf.py:32
+msgid "is greater than"
+msgstr "είναι μεγαλύτερο από"
+
+#: .\conf.py:33
+msgid "is greater than or equal to"
+msgstr "είναι μεγαλύτερο ή ίσο του"
+
+#: .\conf.py:34
+msgid "is less than"
+msgstr "είναι μικρότερο από"
+
+#: .\conf.py:35
+msgid "is less than or equal to"
+msgstr "είναι μικρότερο ή ίσο του"
+
+#: .\conf.py:36 .\conf.py:37
+msgid "starts with"
+msgstr "ξεκινά με"
+
+#: .\conf.py:38 .\conf.py:39
+msgid "ends with"
+msgstr "τελειώνει με"
+
+#: .\conf.py:40
+msgid "is in range"
+msgstr "είναι εντος του εύρους"
+
+#: .\conf.py:42 .\conf.py:43
+msgid "matches regex"
+msgstr "περιέχει regex"
+
+#: .\conf.py:44 .\conf.py:52
+msgid "search"
+msgstr "αναζήτηση"
+
+#: .\conf.py:47
+msgid "is contained by"
+msgstr "περιέχεται σε"
+
+#: .\conf.py:48
+msgid "overlaps"
+msgstr "επικαλύπτεται"
+
+#: .\conf.py:49
+msgid "has key"
+msgstr "έχει το κλειδί"
+
+#: .\conf.py:50
+msgid "has keys"
+msgstr "έχει τα κλειδιά"
+
+#: .\conf.py:51
+msgid "has any keys"
+msgstr "έχει οποιαδήποτε κλειδιά"
+
+#: .\fields.py:178
+msgid "Range query expects two values."
+msgstr "Το ερώτημα εύρους απαιτεί δύο τιμές,"
+
+#: .\filters.py:429
+msgid "Any date"
+msgstr "Οποιαδήποτε ημερομηνία"
+
+#: .\filters.py:430
+msgid "Today"
+msgstr "Σήμερα"
+
+#: .\filters.py:435
+msgid "Past 7 days"
+msgstr "Τις προηγούμενες 7 ημέρες"
+
+#: .\filters.py:439
+msgid "This month"
+msgstr "Αυτό το μήνα"
+
+#: .\filters.py:443
+msgid "This year"
+msgstr "Αυτό το έτος"
+
+#: .\filters.py:446
+msgid "Yesterday"
+msgstr "Χτες"
+
+#: .\filters.py:512
+msgid "Multiple values may be separated by commas."
+msgstr "Οι πολλαπλές τιμές πρέπει να διαχωρίζονται με κόμμα."
+
+#: .\filters.py:591
+#, python-format
+msgid "%s (descending)"
+msgstr "%s (φθίνουσα"
+
+#: .\filters.py:607
+msgid "Ordering"
+msgstr "Ταξινόμηση"
+
+#: .\rest_framework\filterset.py:30
+#: .\templates\django_filters\rest_framework\form.html:5
+msgid "Submit"
+msgstr "Υποβολή"
+
+#: .\templates\django_filters\rest_framework\crispy_form.html:4
+#: .\templates\django_filters\rest_framework\form.html:2
+msgid "Field filters"
+msgstr "Φίλτρα πεδίων"
+
+#: .\utils.py:224
+msgid "exclude"
+msgstr "απέκλεισε"
+
+#: .\widgets.py:57
+msgid "All"
+msgstr "Όλα"
+
+#: .\widgets.py:159
+msgid "Unknown"
+msgstr "Άγνωστο"
+
+#: .\widgets.py:160
+msgid "Yes"
+msgstr "Ναι"
+
+#: .\widgets.py:161
+msgid "No"
+msgstr "Όχι"
diff --git a/venv/Lib/site-packages/django_filters/locale/es/LC_MESSAGES/django.mo b/venv/Lib/site-packages/django_filters/locale/es/LC_MESSAGES/django.mo
new file mode 100644
index 0000000..3338ff7
Binary files /dev/null and b/venv/Lib/site-packages/django_filters/locale/es/LC_MESSAGES/django.mo differ
diff --git a/venv/Lib/site-packages/django_filters/locale/es/LC_MESSAGES/django.po b/venv/Lib/site-packages/django_filters/locale/es/LC_MESSAGES/django.po
new file mode 100644
index 0000000..5c4647f
--- /dev/null
+++ b/venv/Lib/site-packages/django_filters/locale/es/LC_MESSAGES/django.po
@@ -0,0 +1,190 @@
+# Django Filter translation.
+# Copyright (C) 2013
+# This file is distributed under the same license as the django_filter package.
+# Carlos Goce, 2017.
+# Nicolás Stuardo, 2020
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: \n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2020-12-12 17:58-0300\n"
+"PO-Revision-Date: 2020-12-12 17:57-0300\n"
+"Last-Translator: Nicolás Stuardo\n"
+"Language-Team: Spanish (España)\n"
+"Language: es\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"X-Generator: Poedit 2.4.2\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: conf.py:19
+msgid "date"
+msgstr "fecha"
+
+#: conf.py:20
+msgid "year"
+msgstr "año"
+
+#: conf.py:21
+msgid "month"
+msgstr "mes"
+
+#: conf.py:22
+msgid "day"
+msgstr "día"
+
+#: conf.py:23
+msgid "week day"
+msgstr "día de la semana"
+
+#: conf.py:24
+msgid "hour"
+msgstr "hora"
+
+#: conf.py:25
+msgid "minute"
+msgstr "minuto"
+
+#: conf.py:26
+msgid "second"
+msgstr "segundo"
+
+#: conf.py:31 conf.py:32
+msgid "contains"
+msgstr "contiene"
+
+#: conf.py:33
+msgid "is in"
+msgstr "presente en"
+
+#: conf.py:34
+msgid "is greater than"
+msgstr "mayor que"
+
+#: conf.py:35
+msgid "is greater than or equal to"
+msgstr "mayor o igual que"
+
+#: conf.py:36
+msgid "is less than"
+msgstr "menor que"
+
+#: conf.py:37
+msgid "is less than or equal to"
+msgstr "menor o igual que"
+
+#: conf.py:38 conf.py:39
+msgid "starts with"
+msgstr "comienza por"
+
+#: conf.py:40 conf.py:41
+msgid "ends with"
+msgstr "termina por"
+
+#: conf.py:42
+msgid "is in range"
+msgstr "en el rango"
+
+#: conf.py:44 conf.py:45
+msgid "matches regex"
+msgstr "coincide con la expresión regular"
+
+#: conf.py:46 conf.py:54
+msgid "search"
+msgstr "buscar"
+
+#: conf.py:49
+msgid "is contained by"
+msgstr "contenido en"
+
+#: conf.py:50
+msgid "overlaps"
+msgstr "solapado"
+
+#: conf.py:51
+msgid "has key"
+msgstr "contiene la clave"
+
+#: conf.py:52
+msgid "has keys"
+msgstr "contiene las claves"
+
+#: conf.py:53
+msgid "has any keys"
+msgstr "contiene alguna de las claves"
+
+#: fields.py:106
+msgid "Select a lookup."
+msgstr "Seleccione un operador de consulta."
+
+#: fields.py:198
+msgid "Range query expects two values."
+msgstr "Consultar un rango requiere dos valores."
+
+#: filters.py:420
+msgid "Today"
+msgstr "Hoy"
+
+#: filters.py:421
+msgid "Yesterday"
+msgstr "Ayer"
+
+#: filters.py:422
+msgid "Past 7 days"
+msgstr "Últimos 7 días"
+
+#: filters.py:423
+msgid "This month"
+msgstr "Este mes"
+
+#: filters.py:424
+msgid "This year"
+msgstr "Este año"
+
+#: filters.py:522
+msgid "Multiple values may be separated by commas."
+msgstr "Múltiples valores separados por comas."
+
+#: filters.py:695
+#, python-format
+msgid "%s (descending)"
+msgstr "%s (descendente)"
+
+#: filters.py:711
+msgid "Ordering"
+msgstr "Ordenado"
+
+#: rest_framework/filterset.py:31
+#: templates/django_filters/rest_framework/form.html:5
+msgid "Submit"
+msgstr "Enviar"
+
+#: templates/django_filters/rest_framework/crispy_form.html:4
+#: templates/django_filters/rest_framework/form.html:2
+msgid "Field filters"
+msgstr "Filtros de campo"
+
+#: utils.py:294
+msgid "exclude"
+msgstr "excluye"
+
+#: widgets.py:58
+msgid "All"
+msgstr "Todo"
+
+#: widgets.py:160
+msgid "Unknown"
+msgstr "Desconocido"
+
+#: widgets.py:161
+msgid "Yes"
+msgstr "Sí"
+
+#: widgets.py:162
+msgid "No"
+msgstr "No"
+
+#~ msgid "Any date"
+#~ msgstr "Cualquier fecha"
diff --git a/venv/Lib/site-packages/django_filters/locale/es_AR/LC_MESSAGES/django.mo b/venv/Lib/site-packages/django_filters/locale/es_AR/LC_MESSAGES/django.mo
new file mode 100644
index 0000000..7f4778a
Binary files /dev/null and b/venv/Lib/site-packages/django_filters/locale/es_AR/LC_MESSAGES/django.mo differ
diff --git a/venv/Lib/site-packages/django_filters/locale/es_AR/LC_MESSAGES/django.po b/venv/Lib/site-packages/django_filters/locale/es_AR/LC_MESSAGES/django.po
new file mode 100644
index 0000000..751dc8f
--- /dev/null
+++ b/venv/Lib/site-packages/django_filters/locale/es_AR/LC_MESSAGES/django.po
@@ -0,0 +1,47 @@
+# Django Filter translation.
+# Copyright (C) 2013
+# This file is distributed under the same license as the django_filter package.
+# Gonzalo Bustos, 2015.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: \n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2013-07-05 19:24+0200\n"
+"PO-Revision-Date: 2015-10-11 20:53-0300\n"
+"Last-Translator: Gonzalo Bustos\n"
+"Language-Team: Spanish (Argentina)\n"
+"Language: es_AR\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+"X-Generator: Poedit 1.6.10\n"
+
+#: filters.py:51
+msgid "This is an exclusion filter"
+msgstr "Este es un filtro de exclusión"
+
+#: filters.py:158
+msgid "Any date"
+msgstr "Cualquier fecha"
+
+#: filters.py:159
+msgid "Today"
+msgstr "Hoy"
+
+#: filters.py:164
+msgid "Past 7 days"
+msgstr "Últimos 7 días"
+
+#: filters.py:168
+msgid "This month"
+msgstr "Este mes"
+
+#: filters.py:172
+msgid "This year"
+msgstr "Este año"
+
+#: widgets.py:63
+msgid "All"
+msgstr "Todos"
diff --git a/venv/Lib/site-packages/django_filters/locale/fr/LC_MESSAGES/django.mo b/venv/Lib/site-packages/django_filters/locale/fr/LC_MESSAGES/django.mo
new file mode 100644
index 0000000..1b16915
Binary files /dev/null and b/venv/Lib/site-packages/django_filters/locale/fr/LC_MESSAGES/django.mo differ
diff --git a/venv/Lib/site-packages/django_filters/locale/fr/LC_MESSAGES/django.po b/venv/Lib/site-packages/django_filters/locale/fr/LC_MESSAGES/django.po
new file mode 100644
index 0000000..8926219
--- /dev/null
+++ b/venv/Lib/site-packages/django_filters/locale/fr/LC_MESSAGES/django.po
@@ -0,0 +1,47 @@
+# Django Filter translation.
+# Copyright (C) 2013
+# This file is distributed under the same license as the django_filter package.
+# Axel Haustant , 2013.
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE VERSION\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2013-07-05 19:24+0200\n"
+"PO-Revision-Date: 2013-07-05 19:24+0200\n"
+"Last-Translator: Axel Haustant \n"
+"Language-Team: LANGUAGE \n"
+"Language: French\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=(n > 1);\n"
+
+#: filters.py:51
+msgid "This is an exclusion filter"
+msgstr "Ceci est un filtre d'exclusion"
+
+#: filters.py:158
+msgid "Any date"
+msgstr "Toutes les dates"
+
+#: filters.py:159
+msgid "Today"
+msgstr "Aujourd'hui"
+
+#: filters.py:164
+msgid "Past 7 days"
+msgstr "7 derniers jours"
+
+#: filters.py:168
+msgid "This month"
+msgstr "Ce mois-ci"
+
+#: filters.py:172
+msgid "This year"
+msgstr "Cette année"
+
+#: widgets.py:63
+msgid "All"
+msgstr "Tous"
diff --git a/venv/Lib/site-packages/django_filters/locale/it/LC_MESSAGES/django.mo b/venv/Lib/site-packages/django_filters/locale/it/LC_MESSAGES/django.mo
new file mode 100644
index 0000000..f53c4ea
Binary files /dev/null and b/venv/Lib/site-packages/django_filters/locale/it/LC_MESSAGES/django.mo differ
diff --git a/venv/Lib/site-packages/django_filters/locale/it/LC_MESSAGES/django.po b/venv/Lib/site-packages/django_filters/locale/it/LC_MESSAGES/django.po
new file mode 100644
index 0000000..eb16bf2
--- /dev/null
+++ b/venv/Lib/site-packages/django_filters/locale/it/LC_MESSAGES/django.po
@@ -0,0 +1,186 @@
+# Django Filter translation.
+# Copyright (C) 2013
+# This file is distributed under the same license as the django_filter package.
+# Carlos Goce, 2017.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: \n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2017-01-26 20:32+0100\n"
+"PO-Revision-Date: 2017-01-26 20:52+0100\n"
+"Last-Translator: Carlos Goce\n"
+"Language-Team: Spanish (España)\n"
+"Language: es_ES\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"X-Generator: Poedit 1.8.11\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: conf.py:26
+msgid "date"
+msgstr "data"
+
+#: conf.py:27
+msgid "year"
+msgstr "anno"
+
+#: conf.py:28
+msgid "month"
+msgstr "mese"
+
+#: conf.py:29
+msgid "day"
+msgstr "giorno"
+
+#: conf.py:30
+msgid "week day"
+msgstr "giorno della settimana"
+
+#: conf.py:31
+msgid "hour"
+msgstr "ora"
+
+#: conf.py:32
+msgid "minute"
+msgstr "minuto"
+
+#: conf.py:33
+msgid "second"
+msgstr "secondo"
+
+#: conf.py:38 conf.py:39
+msgid "contains"
+msgstr "contiene"
+
+#: conf.py:40
+msgid "is in"
+msgstr "presente in"
+
+#: conf.py:41
+msgid "is greater than"
+msgstr "maggiore di"
+
+#: conf.py:42
+msgid "is greater than or equal to"
+msgstr "maggiore o uguale di"
+
+#: conf.py:43
+msgid "is less than"
+msgstr "minore di"
+
+#: conf.py:44
+msgid "is less than or equal to"
+msgstr "minore o uguale di"
+
+#: conf.py:45 conf.py:46
+msgid "starts with"
+msgstr "comincia per"
+
+#: conf.py:47 conf.py:48
+msgid "ends with"
+msgstr "termina per"
+
+#: conf.py:49
+msgid "is in range"
+msgstr "nell'intervallo"
+
+#: conf.py:51 conf.py:52
+msgid "matches regex"
+msgstr "coincide con la espressione regolare"
+
+#: conf.py:53 conf.py:61
+msgid "search"
+msgstr "cerca"
+
+#: conf.py:56
+msgid "is contained by"
+msgstr "contenuto in"
+
+#: conf.py:57
+msgid "overlaps"
+msgstr "sovrapposto"
+
+#: conf.py:58
+msgid "has key"
+msgstr "contiene la chiave"
+
+#: conf.py:59
+msgid "has keys"
+msgstr "contiene le chiavi"
+
+#: conf.py:60
+msgid "has any keys"
+msgstr "contiene qualsiasi chiave"
+
+#: fields.py:167
+msgid "Range query expects two values."
+msgstr "La query di intervallo richiede due valori"
+
+#: filters.py:443
+msgid "Any date"
+msgstr "Qualsiasi data"
+
+#: filters.py:444
+msgid "Today"
+msgstr "Oggi"
+
+#: filters.py:449
+msgid "Past 7 days"
+msgstr "Ultimi 7 giorni"
+
+#: filters.py:453
+msgid "This month"
+msgstr "Questo mese"
+
+#: filters.py:457
+msgid "This year"
+msgstr "Questo anno"
+
+#: filters.py:460
+msgid "Yesterday"
+msgstr "Ieri"
+
+#: filters.py:526
+msgid "Multiple values may be separated by commas."
+msgstr "Più valori separati da virgole."
+
+#: filters.py:605
+#, python-format
+msgid "%s (descending)"
+msgstr "%s (decrescente)"
+
+#: filters.py:621
+msgid "Ordering"
+msgstr "Ordinamento"
+
+#: utils.py:220
+msgid "exclude"
+msgstr "escludi"
+
+#: widgets.py:71
+msgid "All"
+msgstr "Tutti"
+
+#: widgets.py:119
+msgid "Unknown"
+msgstr "Sconosciuto"
+
+#: widgets.py:120
+msgid "Yes"
+msgstr "Sí"
+
+#: widgets.py:121
+msgid "No"
+msgstr "No"
+
+#: rest_framework/filterset.py:31
+#: templates/django_filters/rest_framework/form.html:5
+msgid "Submit"
+msgstr "Invia"
+
+#: templates/django_filters/rest_framework/crispy_form.html:4
+#: templates/django_filters/rest_framework/form.html:2
+msgid "Field filters"
+msgstr "Filtri del campo"
diff --git a/venv/Lib/site-packages/django_filters/locale/pl/LC_MESSAGES/django.mo b/venv/Lib/site-packages/django_filters/locale/pl/LC_MESSAGES/django.mo
new file mode 100644
index 0000000..fdbf09d
Binary files /dev/null and b/venv/Lib/site-packages/django_filters/locale/pl/LC_MESSAGES/django.mo differ
diff --git a/venv/Lib/site-packages/django_filters/locale/pl/LC_MESSAGES/django.po b/venv/Lib/site-packages/django_filters/locale/pl/LC_MESSAGES/django.po
new file mode 100644
index 0000000..11abbad
--- /dev/null
+++ b/venv/Lib/site-packages/django_filters/locale/pl/LC_MESSAGES/django.po
@@ -0,0 +1,202 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+# FIRST AUTHOR , YEAR.
+#
+#: conf.py:35 conf.py:36 conf.py:49
+msgid ""
+msgstr ""
+"Project-Id-Version: django_filters 0.0.1\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2017-09-01 17:21+0000\n"
+"PO-Revision-Date: 2015-07-25 01:27+0100\n"
+"Last-Translator: Adam Dobrawy \n"
+"Language-Team: Adam Dobrawy \n"
+"Language: pl_PL\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=4; plural=(n==1 ? 0 : (n%10>=2 && n%10<=4) && (n"
+"%100<12 || n%100>14) ? 1 : n!=1 && (n%10>=0 && n%10<=1) || (n%10>=5 && n"
+"%10<=9) || (n%100>=12 && n%100<=14) ? 2 : 3);\n"
+"X-Generator: Poedit 1.5.4\n"
+
+#: conf.py:25
+#, fuzzy
+#| msgid "Any date"
+msgid "date"
+msgstr "Dowolna data"
+
+#: conf.py:26
+#, fuzzy
+#| msgid "This year"
+msgid "year"
+msgstr "Ten rok"
+
+#: conf.py:27
+#, fuzzy
+#| msgid "This month"
+msgid "month"
+msgstr "Ten miesiąc"
+
+#: conf.py:28
+#, fuzzy
+#| msgid "Today"
+msgid "day"
+msgstr "Dziś"
+
+#: conf.py:29
+msgid "week day"
+msgstr "dzień tygodnia"
+
+#: conf.py:30
+msgid "hour"
+msgstr "godzina"
+
+#: conf.py:31
+msgid "minute"
+msgstr "minuta"
+
+#: conf.py:32
+msgid "second"
+msgstr ""
+
+#: conf.py:37 conf.py:38
+msgid "contains"
+msgstr "zawiera"
+
+#: conf.py:39
+msgid "is in"
+msgstr "zawiera się w"
+
+#: conf.py:40
+msgid "is greater than"
+msgstr "powyżej"
+
+#: conf.py:41
+msgid "is greater than or equal to"
+msgstr "powyżej lub równe"
+
+#: conf.py:42
+msgid "is less than"
+msgstr "poniżej"
+
+#: conf.py:43
+msgid "is less than or equal to"
+msgstr "poniżej lub równe"
+
+#: conf.py:44 conf.py:45
+msgid "starts with"
+msgstr "zaczyna się od"
+
+#: conf.py:46 conf.py:47
+msgid "ends with"
+msgstr "kończy się na"
+
+#: conf.py:48
+msgid "is in range"
+msgstr "zawiera się w zakresie"
+
+#: conf.py:50 conf.py:51
+msgid "matches regex"
+msgstr "pasuje do wyrażenia regularnego"
+
+#: conf.py:52 conf.py:60
+msgid "search"
+msgstr "szukaj"
+
+#: conf.py:55
+msgid "is contained by"
+msgstr "zawiera się w"
+
+#: conf.py:56
+msgid "overlaps"
+msgstr ""
+
+#: conf.py:57
+msgid "has key"
+msgstr ""
+
+#: conf.py:58
+msgid "has keys"
+msgstr ""
+
+#: conf.py:59
+msgid "has any keys"
+msgstr ""
+
+#: fields.py:172
+msgid "Range query expects two values."
+msgstr ""
+
+#: filters.py:452
+msgid "Any date"
+msgstr "Dowolna data"
+
+#: filters.py:453
+msgid "Today"
+msgstr "Dziś"
+
+#: filters.py:458
+msgid "Past 7 days"
+msgstr "Ostatnie 7 dni"
+
+#: filters.py:462
+msgid "This month"
+msgstr "Ten miesiąc"
+
+#: filters.py:466
+msgid "This year"
+msgstr "Ten rok"
+
+#: filters.py:469
+msgid "Yesterday"
+msgstr "Wczoraj"
+
+#: filters.py:535
+msgid "Multiple values may be separated by commas."
+msgstr "Wiele wartości można rozdzielić przecinkami"
+
+#: filters.py:614
+#, python-format
+msgid "%s (descending)"
+msgstr "%s (malejąco)"
+
+#: filters.py:630
+msgid "Ordering"
+msgstr "Sortowanie"
+
+#: rest_framework/filterset.py:34
+#: templates/django_filters/rest_framework/form.html:5
+msgid "Submit"
+msgstr ""
+
+#: templates/django_filters/rest_framework/crispy_form.html:4
+#: templates/django_filters/rest_framework/form.html:2
+#, fuzzy
+#| msgid "Filter"
+msgid "Field filters"
+msgstr "Filter"
+
+#: utils.py:225
+msgid "exclude"
+msgstr ""
+
+#: widgets.py:66
+msgid "All"
+msgstr "Wszystko"
+
+#: widgets.py:173
+msgid "Unknown"
+msgstr ""
+
+#: widgets.py:174
+msgid "Yes"
+msgstr "Tak"
+
+#: widgets.py:175
+msgid "No"
+msgstr "Nie"
+
+#~ msgid "This is an exclusion filter"
+#~ msgstr "Jest to filtr wykluczający"
diff --git a/venv/Lib/site-packages/django_filters/locale/pt_BR/LC_MESSAGES/django.mo b/venv/Lib/site-packages/django_filters/locale/pt_BR/LC_MESSAGES/django.mo
new file mode 100644
index 0000000..e9cc1a8
Binary files /dev/null and b/venv/Lib/site-packages/django_filters/locale/pt_BR/LC_MESSAGES/django.mo differ
diff --git a/venv/Lib/site-packages/django_filters/locale/pt_BR/LC_MESSAGES/django.po b/venv/Lib/site-packages/django_filters/locale/pt_BR/LC_MESSAGES/django.po
new file mode 100644
index 0000000..6f8eb0f
--- /dev/null
+++ b/venv/Lib/site-packages/django_filters/locale/pt_BR/LC_MESSAGES/django.po
@@ -0,0 +1,186 @@
+# Django Filter translation.
+# Copyright (C) 2017
+# This file is distributed under the same license as the django_filter package.
+# Anderson Scouto da Silva, 2017.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: \n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2017-12-11 22:04+0100\n"
+"PO-Revision-Date: 2017-12-11 22:07-0200\n"
+"Last-Translator: Anderson Scouto da Silva\n"
+"Language-Team: \n"
+"Language: pt_BR\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"X-Generator: Poedit 1.8.13\n"
+"Plural-Forms: nplurals=2; plural=(n > 1);\n"
+
+#: conf.py:26
+msgid "date"
+msgstr "data"
+
+#: conf.py:27
+msgid "year"
+msgstr "ano"
+
+#: conf.py:28
+msgid "month"
+msgstr "mês"
+
+#: conf.py:29
+msgid "day"
+msgstr "dia"
+
+#: conf.py:30
+msgid "week day"
+msgstr "dia da semana"
+
+#: conf.py:31
+msgid "hour"
+msgstr "hora"
+
+#: conf.py:32
+msgid "minute"
+msgstr "minuto"
+
+#: conf.py:33
+msgid "second"
+msgstr "segundo"
+
+#: conf.py:38 conf.py:39
+msgid "contains"
+msgstr "contém"
+
+#: conf.py:40
+msgid "is in"
+msgstr "presente em"
+
+#: conf.py:41
+msgid "is greater than"
+msgstr "é maior que"
+
+#: conf.py:42
+msgid "is greater than or equal to"
+msgstr "é maior ou igual que"
+
+#: conf.py:43
+msgid "is less than"
+msgstr "é menor que"
+
+#: conf.py:44
+msgid "is less than or equal to"
+msgstr "é menor ou igual que"
+
+#: conf.py:45 conf.py:46
+msgid "starts with"
+msgstr "começa com"
+
+#: conf.py:47 conf.py:48
+msgid "ends with"
+msgstr "termina com"
+
+#: conf.py:49
+msgid "is in range"
+msgstr "está no range"
+
+#: conf.py:51 conf.py:52
+msgid "matches regex"
+msgstr "coincide com a expressão regular"
+
+#: conf.py:53 conf.py:61
+msgid "search"
+msgstr "buscar"
+
+#: conf.py:56
+msgid "is contained by"
+msgstr "está contido por"
+
+#: conf.py:57
+msgid "overlaps"
+msgstr "sobrepõe"
+
+#: conf.py:58
+msgid "has key"
+msgstr "contém a chave"
+
+#: conf.py:59
+msgid "has keys"
+msgstr "contém as chaves"
+
+#: conf.py:60
+msgid "has any keys"
+msgstr "contém uma das chaves"
+
+#: fields.py:167
+msgid "Range query expects two values."
+msgstr "Consulta por range requer dois valores."
+
+#: filters.py:443
+msgid "Any date"
+msgstr "Qualquer data"
+
+#: filters.py:444
+msgid "Today"
+msgstr "Hoje"
+
+#: filters.py:449
+msgid "Past 7 days"
+msgstr "Últimos 7 dias"
+
+#: filters.py:453
+msgid "This month"
+msgstr "Este mês"
+
+#: filters.py:457
+msgid "This year"
+msgstr "Este ano"
+
+#: filters.py:460
+msgid "Yesterday"
+msgstr "Ontem"
+
+#: filters.py:526
+msgid "Multiple values may be separated by commas."
+msgstr "Valores múltiplos podem ser separados por vírgulas."
+
+#: filters.py:605
+#, python-format
+msgid "%s (descending)"
+msgstr "%s (decrescente)"
+
+#: filters.py:621
+msgid "Ordering"
+msgstr "Ordenado"
+
+#: utils.py:220
+msgid "exclude"
+msgstr "excluir"
+
+#: widgets.py:71
+msgid "All"
+msgstr "Tudo"
+
+#: widgets.py:119
+msgid "Unknown"
+msgstr "Desconhecido"
+
+#: widgets.py:120
+msgid "Yes"
+msgstr "Sim"
+
+#: widgets.py:121
+msgid "No"
+msgstr "Não"
+
+#: rest_framework/filterset.py:31
+#: templates/django_filters/rest_framework/form.html:5
+msgid "Submit"
+msgstr "Enviar"
+
+#: templates/django_filters/rest_framework/crispy_form.html:4
+#: templates/django_filters/rest_framework/form.html:2
+msgid "Field filters"
+msgstr "Filtros de campo"
diff --git a/venv/Lib/site-packages/django_filters/locale/ru/LC_MESSAGES/django.mo b/venv/Lib/site-packages/django_filters/locale/ru/LC_MESSAGES/django.mo
new file mode 100644
index 0000000..a4ff1ce
Binary files /dev/null and b/venv/Lib/site-packages/django_filters/locale/ru/LC_MESSAGES/django.mo differ
diff --git a/venv/Lib/site-packages/django_filters/locale/ru/LC_MESSAGES/django.po b/venv/Lib/site-packages/django_filters/locale/ru/LC_MESSAGES/django.po
new file mode 100644
index 0000000..4b54cb5
--- /dev/null
+++ b/venv/Lib/site-packages/django_filters/locale/ru/LC_MESSAGES/django.po
@@ -0,0 +1,189 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+# FIRST AUTHOR , YEAR.
+#
+#: conf.py:27 conf.py:28 conf.py:41
+msgid ""
+msgstr ""
+"Project-Id-Version: django-filter\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2018-01-24 11:03+0500\n"
+"PO-Revision-Date: 2016-09-29 11:47+0300\n"
+"Last-Translator: Mikhail Mitrofanov \n"
+"Language-Team: \n"
+"Language: ru\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=4; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n"
+"%10<=4 && (n%100<12 || n%100>14) ? 1 : n%10==0 || (n%10>=5 && n%10<=9) || (n"
+"%100>=11 && n%100<=14)? 2 : 3);\n"
+"X-Generator: Poedit 1.8.9\n"
+
+#: conf.py:17
+msgid "date"
+msgstr "дата"
+
+#: conf.py:18
+msgid "year"
+msgstr "год"
+
+#: conf.py:19
+msgid "month"
+msgstr "месяц"
+
+#: conf.py:20
+msgid "day"
+msgstr "день"
+
+#: conf.py:21
+msgid "week day"
+msgstr "день недели"
+
+#: conf.py:22
+msgid "hour"
+msgstr "час"
+
+#: conf.py:23
+msgid "minute"
+msgstr "минута"
+
+#: conf.py:24
+msgid "second"
+msgstr "секунда"
+
+#: conf.py:29 conf.py:30
+msgid "contains"
+msgstr "содержит"
+
+#: conf.py:31
+msgid "is in"
+msgstr "в"
+
+#: conf.py:32
+msgid "is greater than"
+msgstr "больше чем"
+
+#: conf.py:33
+msgid "is greater than or equal to"
+msgstr "больше или равно"
+
+#: conf.py:34
+msgid "is less than"
+msgstr "меньше чем"
+
+#: conf.py:35
+msgid "is less than or equal to"
+msgstr "меньше или равно"
+
+#: conf.py:36 conf.py:37
+msgid "starts with"
+msgstr "начинается"
+
+#: conf.py:38 conf.py:39
+msgid "ends with"
+msgstr "заканчивается"
+
+#: conf.py:40
+msgid "is in range"
+msgstr "в диапазоне"
+
+#: conf.py:42 conf.py:43
+msgid "matches regex"
+msgstr "соответствует регулярному выражению"
+
+#: conf.py:44 conf.py:52
+msgid "search"
+msgstr "поиск"
+
+#: conf.py:47
+msgid "is contained by"
+msgstr "содержится в"
+
+#: conf.py:48
+msgid "overlaps"
+msgstr "перекрывается"
+
+#: conf.py:49
+msgid "has key"
+msgstr "имеет ключ"
+
+#: conf.py:50
+msgid "has keys"
+msgstr "имеет ключи"
+
+#: conf.py:51
+msgid "has any keys"
+msgstr "имеет любые ключи"
+
+#: fields.py:178
+msgid "Range query expects two values."
+msgstr "Запрос диапазона ожидает два значения."
+
+#: filters.py:429
+msgid "Any date"
+msgstr "Любая дата"
+
+#: filters.py:430
+msgid "Today"
+msgstr "Сегодня"
+
+#: filters.py:435
+msgid "Past 7 days"
+msgstr "Прошедшие 7 дней"
+
+#: filters.py:439
+msgid "This month"
+msgstr "За этот месяц"
+
+#: filters.py:443
+msgid "This year"
+msgstr "В этом году"
+
+#: filters.py:446
+msgid "Yesterday"
+msgstr "Вчера"
+
+#: filters.py:512
+msgid "Multiple values may be separated by commas."
+msgstr "Несколько значений могут быть разделены запятыми."
+
+#: filters.py:591
+#, python-format
+msgid "%s (descending)"
+msgstr "%s (по убыванию)"
+
+#: filters.py:607
+msgid "Ordering"
+msgstr "Порядок"
+
+#: rest_framework/filterset.py:30
+#: templates/django_filters/rest_framework/form.html:5
+msgid "Submit"
+msgstr "Отправить"
+
+#: templates/django_filters/rest_framework/crispy_form.html:4
+#: templates/django_filters/rest_framework/form.html:2
+msgid "Field filters"
+msgstr "Фильтры по полям"
+
+#: utils.py:224
+msgid "exclude"
+msgstr "исключая"
+
+#: widgets.py:57
+msgid "All"
+msgstr "Все"
+
+#: widgets.py:159
+msgid "Unknown"
+msgstr "Не задано"
+
+#: widgets.py:160
+msgid "Yes"
+msgstr "Да"
+
+#: widgets.py:161
+msgid "No"
+msgstr "Нет"
diff --git a/venv/Lib/site-packages/django_filters/locale/sk/LC_MESSAGES/django.mo b/venv/Lib/site-packages/django_filters/locale/sk/LC_MESSAGES/django.mo
new file mode 100644
index 0000000..d9c67d8
Binary files /dev/null and b/venv/Lib/site-packages/django_filters/locale/sk/LC_MESSAGES/django.mo differ
diff --git a/venv/Lib/site-packages/django_filters/locale/sk/LC_MESSAGES/django.po b/venv/Lib/site-packages/django_filters/locale/sk/LC_MESSAGES/django.po
new file mode 100644
index 0000000..db19f3c
--- /dev/null
+++ b/venv/Lib/site-packages/django_filters/locale/sk/LC_MESSAGES/django.po
@@ -0,0 +1,188 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+# FIRST AUTHOR , YEAR.
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE VERSION\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2018-03-25 19:11+0200\n"
+"PO-Revision-Date: 2018-03-25 19:18+0058\n"
+"Last-Translator: b'Erik Telepovsky '\n"
+"Language-Team: LANGUAGE \n"
+"Language: \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=4; plural=(n % 1 == 0 && n == 1 ? 0 : n % 1 == 0 && n "
+">= 2 && n <= 4 ? 1 : n % 1 != 0 ? 2: 3);\n"
+"X-Translated-Using: django-rosetta 0.8.1\n"
+
+#: conf.py:17
+msgid "date"
+msgstr "dátum"
+
+#: conf.py:18
+msgid "year"
+msgstr "rok"
+
+#: conf.py:19
+msgid "month"
+msgstr "mesiac"
+
+#: conf.py:20
+msgid "day"
+msgstr "deň"
+
+#: conf.py:21
+msgid "week day"
+msgstr "deň týždňa"
+
+#: conf.py:22
+msgid "hour"
+msgstr "hodina"
+
+#: conf.py:23
+msgid "minute"
+msgstr "minúta"
+
+#: conf.py:24
+msgid "second"
+msgstr "sekunda"
+
+#: conf.py:29 conf.py:30
+msgid "contains"
+msgstr "obsahuje"
+
+#: conf.py:31
+msgid "is in"
+msgstr "je v"
+
+#: conf.py:32
+msgid "is greater than"
+msgstr "je vačší než"
+
+#: conf.py:33
+msgid "is greater than or equal to"
+msgstr "je vačší alebo rovný ako"
+
+#: conf.py:34
+msgid "is less than"
+msgstr "je menší než"
+
+#: conf.py:35
+msgid "is less than or equal to"
+msgstr "je menší alebo rovný ako"
+
+#: conf.py:36 conf.py:37
+msgid "starts with"
+msgstr "začína s"
+
+#: conf.py:38 conf.py:39
+msgid "ends with"
+msgstr "končí s"
+
+#: conf.py:40
+msgid "is in range"
+msgstr "je v rozsahu"
+
+#: conf.py:42 conf.py:43
+msgid "matches regex"
+msgstr "spĺňa regex"
+
+#: conf.py:44 conf.py:52
+msgid "search"
+msgstr "hľadať"
+
+#: conf.py:47
+msgid "is contained by"
+msgstr "je obsiahnutý"
+
+#: conf.py:48
+msgid "overlaps"
+msgstr "presahuje"
+
+#: conf.py:49
+msgid "has key"
+msgstr "má kľúč"
+
+#: conf.py:50
+msgid "has keys"
+msgstr "má kľúče"
+
+#: conf.py:51
+msgid "has any keys"
+msgstr "má akékoľvek kľúče"
+
+#: fields.py:178
+msgid "Range query expects two values."
+msgstr "Rozsah očakáva dve hodnoty."
+
+#: filters.py:429
+msgid "Any date"
+msgstr "Akýkoľvek dátum"
+
+#: filters.py:430
+msgid "Today"
+msgstr "Dnes"
+
+#: filters.py:435
+msgid "Past 7 days"
+msgstr "Posledných 7 dní"
+
+#: filters.py:439
+msgid "This month"
+msgstr "Tento mesiac"
+
+#: filters.py:443
+msgid "This year"
+msgstr "Tento rok"
+
+#: filters.py:446
+msgid "Yesterday"
+msgstr "Včera"
+
+#: filters.py:512
+msgid "Multiple values may be separated by commas."
+msgstr "Viacero hodnôt môže byť oddelených čiarkami."
+
+#: filters.py:591
+#, python-format
+msgid "%s (descending)"
+msgstr "%s (klesajúco)"
+
+#: filters.py:607
+msgid "Ordering"
+msgstr "Zoradenie"
+
+#: rest_framework/filterset.py:30
+#: templates/django_filters/rest_framework/form.html:5
+msgid "Submit"
+msgstr "Potvrdiť"
+
+#: templates/django_filters/rest_framework/crispy_form.html:4
+#: templates/django_filters/rest_framework/form.html:2
+msgid "Field filters"
+msgstr "Filtre poľa"
+
+#: utils.py:224
+msgid "exclude"
+msgstr "neobsahuje"
+
+#: widgets.py:57
+msgid "All"
+msgstr "Všetky"
+
+#: widgets.py:159
+msgid "Unknown"
+msgstr "Neznáme"
+
+#: widgets.py:160
+msgid "Yes"
+msgstr "Áno"
+
+#: widgets.py:161
+msgid "No"
+msgstr "Nie"
diff --git a/venv/Lib/site-packages/django_filters/locale/uk/LC_MESSAGES/django.mo b/venv/Lib/site-packages/django_filters/locale/uk/LC_MESSAGES/django.mo
new file mode 100644
index 0000000..e16148d
Binary files /dev/null and b/venv/Lib/site-packages/django_filters/locale/uk/LC_MESSAGES/django.mo differ
diff --git a/venv/Lib/site-packages/django_filters/locale/uk/LC_MESSAGES/django.po b/venv/Lib/site-packages/django_filters/locale/uk/LC_MESSAGES/django.po
new file mode 100644
index 0000000..0314b7f
--- /dev/null
+++ b/venv/Lib/site-packages/django_filters/locale/uk/LC_MESSAGES/django.po
@@ -0,0 +1,184 @@
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: django-filter\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2018-01-24 11:03+0500\n"
+"PO-Revision-Date: 2016-09-29 11:47+0300\n"
+"Last-Translator: Eugena Mikhaylikova \n"
+"Language-Team: TextTempearture\n"
+"Language: uk\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=4; plural=(n % 1 == 0 && n % 10 == 1 && n % 100 != "
+"11 ? 0 : n % 1 == 0 && n % 10 >= 2 && n % 10 <= 4 && (n % 100 < 12 || n % "
+"100 > 14) ? 1 : n % 1 == 0 && (n % 10 ==0 || (n % 10 >=5 && n % 10 <=9) || "
+"(n % 100 >=11 && n % 100 <=14 )) ? 2: 3);\n"
+"X-Generator: Poedit 1.8.9\n"
+
+#: conf.py:17
+msgid "date"
+msgstr "дата"
+
+#: conf.py:18
+msgid "year"
+msgstr "рік"
+
+#: conf.py:19
+msgid "month"
+msgstr "місяць"
+
+#: conf.py:20
+msgid "day"
+msgstr "день"
+
+#: conf.py:21
+msgid "week day"
+msgstr "день тижня"
+
+#: conf.py:22
+msgid "hour"
+msgstr "година"
+
+#: conf.py:23
+msgid "minute"
+msgstr "хвилина"
+
+#: conf.py:24
+msgid "second"
+msgstr "секунда"
+
+#: conf.py:29 conf.py:30
+msgid "contains"
+msgstr "містить"
+
+#: conf.py:31
+msgid "is in"
+msgstr "в"
+
+#: conf.py:32
+msgid "is greater than"
+msgstr "більше ніж"
+
+#: conf.py:33
+msgid "is greater than or equal to"
+msgstr "більше або дорівнює"
+
+#: conf.py:34
+msgid "is less than"
+msgstr "менше ніж"
+
+#: conf.py:35
+msgid "is less than or equal to"
+msgstr "менше або дорівнює"
+
+#: conf.py:36 conf.py:37
+msgid "starts with"
+msgstr "починається"
+
+#: conf.py:38 conf.py:39
+msgid "ends with"
+msgstr "закінчується"
+
+#: conf.py:40
+msgid "is in range"
+msgstr "в діапазоні"
+
+#: conf.py:42 conf.py:43
+msgid "matches regex"
+msgstr "відповідає регулярному виразу"
+
+#: conf.py:44 conf.py:52
+msgid "search"
+msgstr "пошук"
+
+#: conf.py:47
+msgid "is contained by"
+msgstr "міститься в"
+
+#: conf.py:48
+msgid "overlaps"
+msgstr "перекривається"
+
+#: conf.py:49
+msgid "has key"
+msgstr "має ключ"
+
+#: conf.py:50
+msgid "has keys"
+msgstr "має ключі"
+
+#: conf.py:51
+msgid "has any keys"
+msgstr "має будь-які ключі"
+
+#: fields.py:178
+msgid "Range query expects two values."
+msgstr "Запит діапазону очікує два значення."
+
+#: filters.py:429
+msgid "Any date"
+msgstr "Будь-яка дата"
+
+#: filters.py:430
+msgid "Today"
+msgstr "Сьогодні"
+
+#: filters.py:435
+msgid "Past 7 days"
+msgstr "Минулі 7 днів"
+
+#: filters.py:439
+msgid "This month"
+msgstr "За цей місяць"
+
+#: filters.py:443
+msgid "This year"
+msgstr "В цьому році"
+
+#: filters.py:446
+msgid "Yesterday"
+msgstr "Вчора"
+
+#: filters.py:512
+msgid "Multiple values may be separated by commas."
+msgstr "Кілька значень можуть бути розділені комами."
+
+#: filters.py:591
+msgid "%s (descending)"
+msgstr "%s (по спадаючій)"
+
+#: filters.py:607
+msgid "Ordering"
+msgstr "Порядок"
+
+#: rest_framework/filterset.py:30
+#: templates/django_filters/rest_framework/form.html:5
+msgid "Submit"
+msgstr "Відправити"
+
+#: templates/django_filters/rest_framework/crispy_form.html:4
+#: templates/django_filters/rest_framework/form.html:2
+msgid "Field filters"
+msgstr "Фільтри по полях"
+
+#: utils.py:224
+msgid "exclude"
+msgstr "виключаючи"
+
+#: widgets.py:57
+msgid "All"
+msgstr "Усе"
+
+#: widgets.py:159
+msgid "Unknown"
+msgstr "Не задано"
+
+#: widgets.py:160
+msgid "Yes"
+msgstr "Так"
+
+#: widgets.py:161
+msgid "No"
+msgstr "Немає"
diff --git a/venv/Lib/site-packages/django_filters/locale/zh_CN/LC_MESSAGES/django.mo b/venv/Lib/site-packages/django_filters/locale/zh_CN/LC_MESSAGES/django.mo
new file mode 100644
index 0000000..7047a5e
Binary files /dev/null and b/venv/Lib/site-packages/django_filters/locale/zh_CN/LC_MESSAGES/django.mo differ
diff --git a/venv/Lib/site-packages/django_filters/locale/zh_CN/LC_MESSAGES/django.po b/venv/Lib/site-packages/django_filters/locale/zh_CN/LC_MESSAGES/django.po
new file mode 100644
index 0000000..de067b9
--- /dev/null
+++ b/venv/Lib/site-packages/django_filters/locale/zh_CN/LC_MESSAGES/django.po
@@ -0,0 +1,64 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+# Kane Blueriver , 2016.
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE VERSION\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2016-01-30 17:39+0800\n"
+"PO-Revision-Date: 2016-01-30 17:50+0800\n"
+"Last-Translator: Kane Blueriver \n"
+"Language-Team: LANGUAGE \n"
+"Language: \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=1; plural=0;\n"
+
+#: filters.py:62
+msgid "This is an exclusion filter"
+msgstr "未启用该过滤器"
+
+#: filters.py:62
+msgid "Filter"
+msgstr "过滤器"
+
+#: filters.py:264
+msgid "Any date"
+msgstr "任何时刻"
+
+#: filters.py:265
+msgid "Today"
+msgstr "今日"
+
+#: filters.py:270
+msgid "Past 7 days"
+msgstr "过去 7 日"
+
+#: filters.py:274
+msgid "This month"
+msgstr "本月"
+
+#: filters.py:278
+msgid "This year"
+msgstr "今年"
+
+#: filters.py:281
+msgid "Yesterday"
+msgstr "昨日"
+
+#: filterset.py:398 filterset.py:409
+#, python-format
+msgid "%s (descending)"
+msgstr "%s(降序)"
+
+#: filterset.py:411
+msgid "Ordering"
+msgstr "排序"
+
+#: widgets.py:60
+msgid "All"
+msgstr "全部"
diff --git a/venv/Lib/site-packages/django_filters/rest_framework/__init__.py b/venv/Lib/site-packages/django_filters/rest_framework/__init__.py
new file mode 100644
index 0000000..4ffc408
--- /dev/null
+++ b/venv/Lib/site-packages/django_filters/rest_framework/__init__.py
@@ -0,0 +1,4 @@
+# flake8: noqa
+from .backends import DjangoFilterBackend
+from .filters import *
+from .filterset import FilterSet
diff --git a/venv/Lib/site-packages/django_filters/rest_framework/backends.py b/venv/Lib/site-packages/django_filters/rest_framework/backends.py
new file mode 100644
index 0000000..835a67f
--- /dev/null
+++ b/venv/Lib/site-packages/django_filters/rest_framework/backends.py
@@ -0,0 +1,170 @@
+import warnings
+
+from django.template import loader
+from django.utils.deprecation import RenameMethodsBase
+
+from .. import compat, utils
+from . import filters, filterset
+
+
+# TODO: remove metaclass in 2.1
+class RenameAttributes(utils.RenameAttributesBase, RenameMethodsBase):
+ renamed_attributes = (
+ ('default_filter_set', 'filterset_base', utils.MigrationNotice),
+ )
+ renamed_methods = (
+ ('get_filter_class', 'get_filterset_class', utils.MigrationNotice),
+ )
+
+
+class DjangoFilterBackend(metaclass=RenameAttributes):
+ filterset_base = filterset.FilterSet
+ raise_exception = True
+
+ @property
+ def template(self):
+ if compat.is_crispy():
+ return 'django_filters/rest_framework/crispy_form.html'
+ return 'django_filters/rest_framework/form.html'
+
+ def get_filterset(self, request, queryset, view):
+ filterset_class = self.get_filterset_class(view, queryset)
+ if filterset_class is None:
+ return None
+
+ kwargs = self.get_filterset_kwargs(request, queryset, view)
+ return filterset_class(**kwargs)
+
+ def get_filterset_class(self, view, queryset=None):
+ """
+ Return the `FilterSet` class used to filter the queryset.
+ """
+ filterset_class = getattr(view, 'filterset_class', None)
+ filterset_fields = getattr(view, 'filterset_fields', None)
+
+ # TODO: remove assertion in 2.1
+ if filterset_class is None and hasattr(view, 'filter_class'):
+ utils.deprecate(
+ "`%s.filter_class` attribute should be renamed `filterset_class`."
+ % view.__class__.__name__)
+ filterset_class = getattr(view, 'filter_class', None)
+
+ # TODO: remove assertion in 2.1
+ if filterset_fields is None and hasattr(view, 'filter_fields'):
+ utils.deprecate(
+ "`%s.filter_fields` attribute should be renamed `filterset_fields`."
+ % view.__class__.__name__)
+ filterset_fields = getattr(view, 'filter_fields', None)
+
+ if filterset_class:
+ filterset_model = filterset_class._meta.model
+
+ # FilterSets do not need to specify a Meta class
+ if filterset_model and queryset is not None:
+ assert issubclass(queryset.model, filterset_model), \
+ 'FilterSet model %s does not match queryset model %s' % \
+ (filterset_model, queryset.model)
+
+ return filterset_class
+
+ if filterset_fields and queryset is not None:
+ MetaBase = getattr(self.filterset_base, 'Meta', object)
+
+ class AutoFilterSet(self.filterset_base):
+ class Meta(MetaBase):
+ model = queryset.model
+ fields = filterset_fields
+
+ return AutoFilterSet
+
+ return None
+
+ def get_filterset_kwargs(self, request, queryset, view):
+ return {
+ 'data': request.query_params,
+ 'queryset': queryset,
+ 'request': request,
+ }
+
+ def filter_queryset(self, request, queryset, view):
+ filterset = self.get_filterset(request, queryset, view)
+ if filterset is None:
+ return queryset
+
+ if not filterset.is_valid() and self.raise_exception:
+ raise utils.translate_validation(filterset.errors)
+ return filterset.qs
+
+ def to_html(self, request, queryset, view):
+ filterset = self.get_filterset(request, queryset, view)
+ if filterset is None:
+ return None
+
+ template = loader.get_template(self.template)
+ context = {'filter': filterset}
+ return template.render(context, request)
+
+ def get_coreschema_field(self, field):
+ if isinstance(field, filters.NumberFilter):
+ field_cls = compat.coreschema.Number
+ else:
+ field_cls = compat.coreschema.String
+ return field_cls(
+ description=str(field.extra.get('help_text', ''))
+ )
+
+ def get_schema_fields(self, view):
+ # This is not compatible with widgets where the query param differs from the
+ # filter's attribute name. Notably, this includes `MultiWidget`, where query
+ # params will be of the format `_0`, `_1`, etc...
+ assert compat.coreapi is not None, 'coreapi must be installed to use `get_schema_fields()`'
+ assert compat.coreschema is not None, 'coreschema must be installed to use `get_schema_fields()`'
+
+ try:
+ queryset = view.get_queryset()
+ except Exception:
+ queryset = None
+ warnings.warn(
+ "{} is not compatible with schema generation".format(view.__class__)
+ )
+
+ filterset_class = self.get_filterset_class(view, queryset)
+
+ return [] if not filterset_class else [
+ compat.coreapi.Field(
+ name=field_name,
+ required=field.extra['required'],
+ location='query',
+ schema=self.get_coreschema_field(field)
+ ) for field_name, field in filterset_class.base_filters.items()
+ ]
+
+ def get_schema_operation_parameters(self, view):
+ try:
+ queryset = view.get_queryset()
+ except Exception:
+ queryset = None
+ warnings.warn(
+ "{} is not compatible with schema generation".format(view.__class__)
+ )
+
+ filterset_class = self.get_filterset_class(view, queryset)
+
+ if not filterset_class:
+ return []
+
+ parameters = []
+ for field_name, field in filterset_class.base_filters.items():
+ parameter = {
+ 'name': field_name,
+ 'required': field.extra['required'],
+ 'in': 'query',
+ 'description': field.label if field.label is not None else field_name,
+ 'schema': {
+ 'type': 'string',
+ },
+ }
+ if field.extra and 'choices' in field.extra:
+ parameter['schema']['enum'] = [c[0] for c in field.extra['choices']]
+ parameters.append(parameter)
+ return parameters
diff --git a/venv/Lib/site-packages/django_filters/rest_framework/filters.py b/venv/Lib/site-packages/django_filters/rest_framework/filters.py
new file mode 100644
index 0000000..4c5755e
--- /dev/null
+++ b/venv/Lib/site-packages/django_filters/rest_framework/filters.py
@@ -0,0 +1,13 @@
+from django_filters import filters
+
+from ..filters import * # noqa
+from ..widgets import BooleanWidget
+
+__all__ = filters.__all__
+
+
+class BooleanFilter(filters.BooleanFilter):
+ def __init__(self, *args, **kwargs):
+ kwargs.setdefault('widget', BooleanWidget)
+
+ super().__init__(*args, **kwargs)
diff --git a/venv/Lib/site-packages/django_filters/rest_framework/filterset.py b/venv/Lib/site-packages/django_filters/rest_framework/filterset.py
new file mode 100644
index 0000000..8c42304
--- /dev/null
+++ b/venv/Lib/site-packages/django_filters/rest_framework/filterset.py
@@ -0,0 +1,40 @@
+from copy import deepcopy
+
+from django.db import models
+from django.utils.translation import gettext_lazy as _
+
+from django_filters import filterset
+
+from .. import compat
+from .filters import BooleanFilter, IsoDateTimeFilter
+
+FILTER_FOR_DBFIELD_DEFAULTS = deepcopy(filterset.FILTER_FOR_DBFIELD_DEFAULTS)
+FILTER_FOR_DBFIELD_DEFAULTS.update({
+ models.DateTimeField: {'filter_class': IsoDateTimeFilter},
+ models.BooleanField: {'filter_class': BooleanFilter},
+ models.NullBooleanField: {'filter_class': BooleanFilter},
+})
+
+
+class FilterSet(filterset.FilterSet):
+ FILTER_DEFAULTS = FILTER_FOR_DBFIELD_DEFAULTS
+
+ @property
+ def form(self):
+ form = super().form
+
+ if compat.is_crispy():
+ from crispy_forms.helper import FormHelper
+ from crispy_forms.layout import Layout, Submit
+
+ layout_components = list(form.fields.keys()) + [
+ Submit('', _('Submit'), css_class='btn-default'),
+ ]
+ helper = FormHelper()
+ helper.form_method = 'GET'
+ helper.template_pack = 'bootstrap3'
+ helper.layout = Layout(*layout_components)
+
+ form.helper = helper
+
+ return form
diff --git a/venv/Lib/site-packages/django_filters/templates/django_filters/rest_framework/crispy_form.html b/venv/Lib/site-packages/django_filters/templates/django_filters/rest_framework/crispy_form.html
new file mode 100644
index 0000000..171767c
--- /dev/null
+++ b/venv/Lib/site-packages/django_filters/templates/django_filters/rest_framework/crispy_form.html
@@ -0,0 +1,5 @@
+{% load crispy_forms_tags %}
+{% load i18n %}
+
+
+
diff --git a/venv/Lib/site-packages/django_filters/templates/django_filters/widgets/multiwidget.html b/venv/Lib/site-packages/django_filters/templates/django_filters/widgets/multiwidget.html
new file mode 100644
index 0000000..089ddb2
--- /dev/null
+++ b/venv/Lib/site-packages/django_filters/templates/django_filters/widgets/multiwidget.html
@@ -0,0 +1 @@
+{% for widget in widget.subwidgets %}{% include widget.template_name %}{% if forloop.first %}-{% endif %}{% endfor %}
diff --git a/venv/Lib/site-packages/django_filters/utils.py b/venv/Lib/site-packages/django_filters/utils.py
new file mode 100644
index 0000000..67a071a
--- /dev/null
+++ b/venv/Lib/site-packages/django_filters/utils.py
@@ -0,0 +1,328 @@
+import warnings
+from collections import OrderedDict
+
+from django.conf import settings
+from django.core.exceptions import FieldDoesNotExist, FieldError
+from django.db import models
+from django.db.models.constants import LOOKUP_SEP
+from django.db.models.expressions import Expression
+from django.db.models.fields.related import ForeignObjectRel, RelatedField
+from django.utils import timezone
+from django.utils.encoding import force_str
+from django.utils.text import capfirst
+from django.utils.translation import gettext as _
+
+from .exceptions import FieldLookupError
+
+
+def deprecate(msg, level_modifier=0):
+ warnings.warn(msg, MigrationNotice, stacklevel=3 + level_modifier)
+
+
+class MigrationNotice(DeprecationWarning):
+ url = 'https://django-filter.readthedocs.io/en/main/guide/migration.html'
+
+ def __init__(self, message):
+ super().__init__('%s See: %s' % (message, self.url))
+
+
+class RenameAttributesBase(type):
+ """
+ Handles the deprecation paths when renaming an attribute.
+
+ It does the following:
+ - Defines accessors that redirect to the renamed attributes.
+ - Complain whenever an old attribute is accessed.
+
+ This is conceptually based on `django.utils.deprecation.RenameMethodsBase`.
+ """
+ renamed_attributes = ()
+
+ def __new__(metacls, name, bases, attrs):
+ # remove old attributes before creating class
+ old_names = [r[0] for r in metacls.renamed_attributes]
+ old_names = [name for name in old_names if name in attrs]
+ old_attrs = {name: attrs.pop(name) for name in old_names}
+
+ # get a handle to any accessors defined on the class
+ cls_getattr = attrs.pop('__getattr__', None)
+ cls_setattr = attrs.pop('__setattr__', None)
+
+ new_class = super().__new__(metacls, name, bases, attrs)
+
+ def __getattr__(self, name):
+ name = type(self).get_name(name)
+ if cls_getattr is not None:
+ return cls_getattr(self, name)
+ elif hasattr(super(new_class, self), '__getattr__'):
+ return super(new_class, self).__getattr__(name)
+ return self.__getattribute__(name)
+
+ def __setattr__(self, name, value):
+ name = type(self).get_name(name)
+ if cls_setattr is not None:
+ return cls_setattr(self, name, value)
+ return super(new_class, self).__setattr__(name, value)
+
+ new_class.__getattr__ = __getattr__
+ new_class.__setattr__ = __setattr__
+
+ # set renamed attributes
+ for name, value in old_attrs.items():
+ setattr(new_class, name, value)
+
+ return new_class
+
+ def get_name(metacls, name):
+ """
+ Get the real attribute name. If the attribute has been renamed,
+ the new name will be returned and a deprecation warning issued.
+ """
+ for renamed_attribute in metacls.renamed_attributes:
+ old_name, new_name, deprecation_warning = renamed_attribute
+
+ if old_name == name:
+ warnings.warn("`%s.%s` attribute should be renamed `%s`."
+ % (metacls.__name__, old_name, new_name),
+ deprecation_warning, 3)
+ return new_name
+
+ return name
+
+ def __getattr__(metacls, name):
+ return super().__getattribute__(metacls.get_name(name))
+
+ def __setattr__(metacls, name, value):
+ return super().__setattr__(metacls.get_name(name), value)
+
+
+def try_dbfield(fn, field_class):
+ """
+ Try ``fn`` with the DB ``field_class`` by walking its
+ MRO until a result is found.
+
+ ex::
+ _try_dbfield(field_dict.get, models.CharField)
+
+ """
+ # walk the mro, as field_class could be a derived model field.
+ for cls in field_class.mro():
+ # skip if cls is models.Field
+ if cls is models.Field:
+ continue
+
+ data = fn(cls)
+ if data:
+ return data
+
+
+def get_all_model_fields(model):
+ opts = model._meta
+
+ return [
+ f.name for f in sorted(opts.fields + opts.many_to_many)
+ if not isinstance(f, models.AutoField) and
+ not (getattr(f.remote_field, 'parent_link', False))
+ ]
+
+
+def get_model_field(model, field_name):
+ """
+ Get a ``model`` field, traversing relationships
+ in the ``field_name``.
+
+ ex::
+
+ f = get_model_field(Book, 'author__first_name')
+
+ """
+ fields = get_field_parts(model, field_name)
+ return fields[-1] if fields else None
+
+
+def get_field_parts(model, field_name):
+ """
+ Get the field parts that represent the traversable relationships from the
+ base ``model`` to the final field, described by ``field_name``.
+
+ ex::
+
+ >>> parts = get_field_parts(Book, 'author__first_name')
+ >>> [p.verbose_name for p in parts]
+ ['author', 'first name']
+
+ """
+ parts = field_name.split(LOOKUP_SEP)
+ opts = model._meta
+ fields = []
+
+ # walk relationships
+ for name in parts:
+ try:
+ field = opts.get_field(name)
+ except FieldDoesNotExist:
+ return None
+
+ fields.append(field)
+ try:
+ if isinstance(field, RelatedField):
+ opts = field.remote_field.model._meta
+ elif isinstance(field, ForeignObjectRel):
+ opts = field.related_model._meta
+ except AttributeError:
+ # Lazy relationships are not resolved until registry is populated.
+ raise RuntimeError(
+ "Unable to resolve relationship `%s` for `%s`. Django is most "
+ "likely not initialized, and its apps registry not populated. "
+ "Ensure Django has finished setup before loading `FilterSet`s."
+ % (field_name, model._meta.label))
+
+ return fields
+
+
+def resolve_field(model_field, lookup_expr):
+ """
+ Resolves a ``lookup_expr`` into its final output field, given
+ the initial ``model_field``. The lookup expression should only contain
+ transforms and lookups, not intermediary model field parts.
+
+ Note:
+ This method is based on django.db.models.sql.query.Query.build_lookup
+
+ For more info on the lookup API:
+ https://docs.djangoproject.com/en/stable/ref/models/lookups/
+
+ """
+ query = model_field.model._default_manager.all().query
+ lhs = Expression(model_field)
+ lookups = lookup_expr.split(LOOKUP_SEP)
+
+ assert len(lookups) > 0
+
+ try:
+ while lookups:
+ name = lookups[0]
+ args = (lhs, name)
+ # If there is just one part left, try first get_lookup() so
+ # that if the lhs supports both transform and lookup for the
+ # name, then lookup will be picked.
+ if len(lookups) == 1:
+ final_lookup = lhs.get_lookup(name)
+ if not final_lookup:
+ # We didn't find a lookup. We are going to interpret
+ # the name as transform, and do an Exact lookup against
+ # it.
+ lhs = query.try_transform(*args)
+ final_lookup = lhs.get_lookup('exact')
+ return lhs.output_field, final_lookup.lookup_name
+ lhs = query.try_transform(*args)
+ lookups = lookups[1:]
+ except FieldError as e:
+ raise FieldLookupError(model_field, lookup_expr) from e
+
+
+def handle_timezone(value, is_dst=None):
+ if settings.USE_TZ and timezone.is_naive(value):
+ return timezone.make_aware(value, timezone.get_current_timezone(), is_dst)
+ elif not settings.USE_TZ and timezone.is_aware(value):
+ return timezone.make_naive(value, timezone.utc)
+ return value
+
+
+def verbose_field_name(model, field_name):
+ """
+ Get the verbose name for a given ``field_name``. The ``field_name``
+ will be traversed across relationships. Returns '[invalid name]' for
+ any field name that cannot be traversed.
+
+ ex::
+
+ >>> verbose_field_name(Article, 'author__name')
+ 'author name'
+
+ """
+ if field_name is None:
+ return '[invalid name]'
+
+ parts = get_field_parts(model, field_name)
+ if not parts:
+ return '[invalid name]'
+
+ names = []
+ for part in parts:
+ if isinstance(part, ForeignObjectRel):
+ if part.related_name:
+ names.append(part.related_name.replace('_', ' '))
+ else:
+ return '[invalid name]'
+ else:
+ names.append(force_str(part.verbose_name))
+
+ return ' '.join(names)
+
+
+def verbose_lookup_expr(lookup_expr):
+ """
+ Get a verbose, more humanized expression for a given ``lookup_expr``.
+ Each part in the expression is looked up in the ``FILTERS_VERBOSE_LOOKUPS``
+ dictionary. Missing keys will simply default to itself.
+
+ ex::
+
+ >>> verbose_lookup_expr('year__lt')
+ 'year is less than'
+
+ # with `FILTERS_VERBOSE_LOOKUPS = {}`
+ >>> verbose_lookup_expr('year__lt')
+ 'year lt'
+
+ """
+ from .conf import settings as app_settings
+
+ VERBOSE_LOOKUPS = app_settings.VERBOSE_LOOKUPS or {}
+ lookups = [
+ force_str(VERBOSE_LOOKUPS.get(lookup, _(lookup)))
+ for lookup in lookup_expr.split(LOOKUP_SEP)
+ ]
+
+ return ' '.join(lookups)
+
+
+def label_for_filter(model, field_name, lookup_expr, exclude=False):
+ """
+ Create a generic label suitable for a filter.
+
+ ex::
+
+ >>> label_for_filter(Article, 'author__name', 'in')
+ 'auther name is in'
+
+ """
+ name = verbose_field_name(model, field_name)
+ verbose_expression = [_('exclude'), name] if exclude else [name]
+
+ # iterable lookups indicate a LookupTypeField, which should not be verbose
+ if isinstance(lookup_expr, str):
+ verbose_expression += [verbose_lookup_expr(lookup_expr)]
+
+ verbose_expression = [force_str(part) for part in verbose_expression if part]
+ verbose_expression = capfirst(' '.join(verbose_expression))
+
+ return verbose_expression
+
+
+def translate_validation(error_dict):
+ """
+ Translate a Django ErrorDict into its DRF ValidationError.
+ """
+ # it's necessary to lazily import the exception, as it can otherwise create
+ # an import loop when importing django_filters inside the project settings.
+ from rest_framework.exceptions import ErrorDetail, ValidationError
+
+ exc = OrderedDict(
+ (key, [ErrorDetail(e.message % (e.params or ()), code=e.code)
+ for e in error_list])
+ for key, error_list in error_dict.as_data().items()
+ )
+
+ return ValidationError(exc)
diff --git a/venv/Lib/site-packages/django_filters/views.py b/venv/Lib/site-packages/django_filters/views.py
new file mode 100644
index 0000000..e9160ad
--- /dev/null
+++ b/venv/Lib/site-packages/django_filters/views.py
@@ -0,0 +1,116 @@
+from django.core.exceptions import ImproperlyConfigured
+from django.views.generic import View
+from django.views.generic.list import (
+ MultipleObjectMixin,
+ MultipleObjectTemplateResponseMixin
+)
+
+from .constants import ALL_FIELDS
+from .filterset import filterset_factory
+from .utils import MigrationNotice, RenameAttributesBase
+
+
+# TODO: remove metaclass in 2.1
+class FilterMixinRenames(RenameAttributesBase):
+ renamed_attributes = (
+ ('filter_fields', 'filterset_fields', MigrationNotice),
+ )
+
+
+class FilterMixin(metaclass=FilterMixinRenames):
+ """
+ A mixin that provides a way to show and handle a FilterSet in a request.
+ """
+ filterset_class = None
+ filterset_fields = ALL_FIELDS
+ strict = True
+
+ def get_filterset_class(self):
+ """
+ Returns the filterset class to use in this view
+ """
+ if self.filterset_class:
+ return self.filterset_class
+ elif self.model:
+ return filterset_factory(model=self.model, fields=self.filterset_fields)
+ else:
+ msg = "'%s' must define 'filterset_class' or 'model'"
+ raise ImproperlyConfigured(msg % self.__class__.__name__)
+
+ def get_filterset(self, filterset_class):
+ """
+ Returns an instance of the filterset to be used in this view.
+ """
+ kwargs = self.get_filterset_kwargs(filterset_class)
+ return filterset_class(**kwargs)
+
+ def get_filterset_kwargs(self, filterset_class):
+ """
+ Returns the keyword arguments for instantiating the filterset.
+ """
+ kwargs = {
+ 'data': self.request.GET or None,
+ 'request': self.request,
+ }
+ try:
+ kwargs.update({
+ 'queryset': self.get_queryset(),
+ })
+ except ImproperlyConfigured:
+ # ignore the error here if the filterset has a model defined
+ # to acquire a queryset from
+ if filterset_class._meta.model is None:
+ msg = ("'%s' does not define a 'model' and the view '%s' does "
+ "not return a valid queryset from 'get_queryset'. You "
+ "must fix one of them.")
+ args = (filterset_class.__name__, self.__class__.__name__)
+ raise ImproperlyConfigured(msg % args)
+ return kwargs
+
+ def get_strict(self):
+ return self.strict
+
+
+class BaseFilterView(FilterMixin, MultipleObjectMixin, View):
+
+ def get(self, request, *args, **kwargs):
+ filterset_class = self.get_filterset_class()
+ self.filterset = self.get_filterset(filterset_class)
+
+ if not self.filterset.is_bound or self.filterset.is_valid() or not self.get_strict():
+ self.object_list = self.filterset.qs
+ else:
+ self.object_list = self.filterset.queryset.none()
+
+ context = self.get_context_data(filter=self.filterset,
+ object_list=self.object_list)
+ return self.render_to_response(context)
+
+
+class FilterView(MultipleObjectTemplateResponseMixin, BaseFilterView):
+ """
+ Render some list of objects with filter, set by `self.model` or
+ `self.queryset`.
+ `self.queryset` can actually be any iterable of items, not just a queryset.
+ """
+ template_name_suffix = '_filter'
+
+
+def object_filter(request, model=None, queryset=None, template_name=None,
+ extra_context=None, context_processors=None,
+ filter_class=None):
+ class ECFilterView(FilterView):
+ """Handle the extra_context from the functional object_filter view"""
+ def get_context_data(self, **kwargs):
+ context = super().get_context_data(**kwargs)
+ extra_context = self.kwargs.get('extra_context') or {}
+ for k, v in extra_context.items():
+ if callable(v):
+ v = v()
+ context[k] = v
+ return context
+
+ kwargs = dict(model=model, queryset=queryset, template_name=template_name,
+ filterset_class=filter_class)
+ view = ECFilterView.as_view(**kwargs)
+ return view(request, extra_context=extra_context)
diff --git a/venv/Lib/site-packages/django_filters/widgets.py b/venv/Lib/site-packages/django_filters/widgets.py
new file mode 100644
index 0000000..889fd81
--- /dev/null
+++ b/venv/Lib/site-packages/django_filters/widgets.py
@@ -0,0 +1,270 @@
+from collections.abc import Iterable
+from copy import deepcopy
+from itertools import chain
+from re import search, sub
+
+from django import forms
+from django.db.models.fields import BLANK_CHOICE_DASH
+from django.forms.utils import flatatt
+from django.utils.datastructures import MultiValueDict
+from django.utils.encoding import force_str
+from django.utils.http import urlencode
+from django.utils.safestring import mark_safe
+from django.utils.translation import gettext as _
+
+
+class LinkWidget(forms.Widget):
+ def __init__(self, attrs=None, choices=()):
+ super().__init__(attrs)
+
+ self.choices = choices
+
+ def value_from_datadict(self, data, files, name):
+ value = super().value_from_datadict(data, files, name)
+ self.data = data
+ return value
+
+ def render(self, name, value, attrs=None, choices=(), renderer=None):
+ if not hasattr(self, 'data'):
+ self.data = {}
+ if value is None:
+ value = ''
+ final_attrs = self.build_attrs(self.attrs, extra_attrs=attrs)
+ output = ['
` tag.
+ # See https://w3c.github.io/html/grouping-content.html#the-p-element
+ 'address', 'article', 'aside', 'blockquote', 'details', 'div', 'dl',
+ 'fieldset', 'figcaption', 'figure', 'footer', 'form', 'h1', 'h2', 'h3',
+ 'h4', 'h5', 'h6', 'header', 'hgroup', 'hr', 'main', 'menu', 'nav', 'ol',
+ 'p', 'pre', 'section', 'table', 'ul',
+ # Other elements which Markdown should not be mucking up the contents of.
+ 'canvas', 'colgroup', 'dd', 'body', 'dt', 'group', 'iframe', 'li', 'legend',
+ 'math', 'map', 'noscript', 'output', 'object', 'option', 'progress', 'script',
+ 'style', 'summary', 'tbody', 'td', 'textarea', 'tfoot', 'th', 'thead', 'tr', 'video'
+ ]
+
+ self.registeredExtensions = []
+ self.docType = ""
+ self.stripTopLevelTags = True
+
+ self.build_parser()
+
+ self.references = {}
+ self.htmlStash = util.HtmlStash()
+ self.registerExtensions(extensions=kwargs.get('extensions', []),
+ configs=kwargs.get('extension_configs', {}))
+ self.set_output_format(kwargs.get('output_format', 'xhtml'))
+ self.reset()
+
+ def build_parser(self):
+ """ Build the parser from the various parts. """
+ self.preprocessors = build_preprocessors(self)
+ self.parser = build_block_parser(self)
+ self.inlinePatterns = build_inlinepatterns(self)
+ self.treeprocessors = build_treeprocessors(self)
+ self.postprocessors = build_postprocessors(self)
+ return self
+
+ def registerExtensions(self, extensions, configs):
+ """
+ Register extensions with this instance of Markdown.
+
+ Keyword arguments:
+
+ * extensions: A list of extensions, which can either
+ be strings or objects.
+ * configs: A dictionary mapping extension names to config options.
+
+ """
+ for ext in extensions:
+ if isinstance(ext, str):
+ ext = self.build_extension(ext, configs.get(ext, {}))
+ if isinstance(ext, Extension):
+ ext._extendMarkdown(self)
+ logger.debug(
+ 'Successfully loaded extension "%s.%s".'
+ % (ext.__class__.__module__, ext.__class__.__name__)
+ )
+ elif ext is not None:
+ raise TypeError(
+ 'Extension "{}.{}" must be of type: "{}.{}"'.format(
+ ext.__class__.__module__, ext.__class__.__name__,
+ Extension.__module__, Extension.__name__
+ )
+ )
+ return self
+
+ def build_extension(self, ext_name, configs):
+ """
+ Build extension from a string name, then return an instance.
+
+ First attempt to load an entry point. The string name must be registered as an entry point in the
+ `markdown.extensions` group which points to a subclass of the `markdown.extensions.Extension` class.
+ If multiple distributions have registered the same name, the first one found is returned.
+
+ If no entry point is found, assume dot notation (`path.to.module:ClassName`). Load the specified class and
+ return an instance. If no class is specified, import the module and call a `makeExtension` function and return
+ the Extension instance returned by that function.
+ """
+ configs = dict(configs)
+
+ entry_points = [ep for ep in util.INSTALLED_EXTENSIONS if ep.name == ext_name]
+ if entry_points:
+ ext = entry_points[0].load()
+ return ext(**configs)
+
+ # Get class name (if provided): `path.to.module:ClassName`
+ ext_name, class_name = ext_name.split(':', 1) if ':' in ext_name else (ext_name, '')
+
+ try:
+ module = importlib.import_module(ext_name)
+ logger.debug(
+ 'Successfully imported extension module "%s".' % ext_name
+ )
+ except ImportError as e:
+ message = 'Failed loading extension "%s".' % ext_name
+ e.args = (message,) + e.args[1:]
+ raise
+
+ if class_name:
+ # Load given class name from module.
+ return getattr(module, class_name)(**configs)
+ else:
+ # Expect makeExtension() function to return a class.
+ try:
+ return module.makeExtension(**configs)
+ except AttributeError as e:
+ message = e.args[0]
+ message = "Failed to initiate extension " \
+ "'%s': %s" % (ext_name, message)
+ e.args = (message,) + e.args[1:]
+ raise
+
+ def registerExtension(self, extension):
+ """ This gets called by the extension """
+ self.registeredExtensions.append(extension)
+ return self
+
+ def reset(self):
+ """
+ Resets all state variables so that we can start with a new text.
+ """
+ self.htmlStash.reset()
+ self.references.clear()
+
+ for extension in self.registeredExtensions:
+ if hasattr(extension, 'reset'):
+ extension.reset()
+
+ return self
+
+ def set_output_format(self, format):
+ """ Set the output format for the class instance. """
+ self.output_format = format.lower().rstrip('145') # ignore num
+ try:
+ self.serializer = self.output_formats[self.output_format]
+ except KeyError as e:
+ valid_formats = list(self.output_formats.keys())
+ valid_formats.sort()
+ message = 'Invalid Output Format: "%s". Use one of %s.' \
+ % (self.output_format,
+ '"' + '", "'.join(valid_formats) + '"')
+ e.args = (message,) + e.args[1:]
+ raise
+ return self
+
+ def is_block_level(self, tag):
+ """Check if the tag is a block level HTML tag."""
+ if isinstance(tag, str):
+ return tag.lower().rstrip('/') in self.block_level_elements
+ # Some ElementTree tags are not strings, so return False.
+ return False
+
+ def convert(self, source):
+ """
+ Convert markdown to serialized XHTML or HTML.
+
+ Keyword arguments:
+
+ * source: Source text as a Unicode string.
+
+ Markdown processing takes place in five steps:
+
+ 1. A bunch of "preprocessors" munge the input text.
+ 2. BlockParser() parses the high-level structural elements of the
+ pre-processed text into an ElementTree.
+ 3. A bunch of "treeprocessors" are run against the ElementTree. One
+ such treeprocessor runs InlinePatterns against the ElementTree,
+ detecting inline markup.
+ 4. Some post-processors are run against the text after the ElementTree
+ has been serialized into text.
+ 5. The output is written to a string.
+
+ """
+
+ # Fixup the source text
+ if not source.strip():
+ return '' # a blank unicode string
+
+ try:
+ source = str(source)
+ except UnicodeDecodeError as e: # pragma: no cover
+ # Customise error message while maintaining original trackback
+ e.reason += '. -- Note: Markdown only accepts unicode input!'
+ raise
+
+ # Split into lines and run the line preprocessors.
+ self.lines = source.split("\n")
+ for prep in self.preprocessors:
+ self.lines = prep.run(self.lines)
+
+ # Parse the high-level elements.
+ root = self.parser.parseDocument(self.lines).getroot()
+
+ # Run the tree-processors
+ for treeprocessor in self.treeprocessors:
+ newRoot = treeprocessor.run(root)
+ if newRoot is not None:
+ root = newRoot
+
+ # Serialize _properly_. Strip top-level tags.
+ output = self.serializer(root)
+ if self.stripTopLevelTags:
+ try:
+ start = output.index(
+ '<%s>' % self.doc_tag) + len(self.doc_tag) + 2
+ end = output.rindex('%s>' % self.doc_tag)
+ output = output[start:end].strip()
+ except ValueError as e: # pragma: no cover
+ if output.strip().endswith('<%s />' % self.doc_tag):
+ # We have an empty document
+ output = ''
+ else:
+ # We have a serious problem
+ raise ValueError('Markdown failed to strip top-level '
+ 'tags. Document=%r' % output.strip()) from e
+
+ # Run the text post-processors
+ for pp in self.postprocessors:
+ output = pp.run(output)
+
+ return output.strip()
+
+ def convertFile(self, input=None, output=None, encoding=None):
+ """Converts a markdown file and returns the HTML as a unicode string.
+
+ Decodes the file using the provided encoding (defaults to utf-8),
+ passes the file content to markdown, and outputs the html to either
+ the provided stream or the file with provided name, using the same
+ encoding as the source file. The 'xmlcharrefreplace' error handler is
+ used when encoding the output.
+
+ **Note:** This is the only place that decoding and encoding of unicode
+ takes place in Python-Markdown. (All other code is unicode-in /
+ unicode-out.)
+
+ Keyword arguments:
+
+ * input: File object or path. Reads from stdin if `None`.
+ * output: File object or path. Writes to stdout if `None`.
+ * encoding: Encoding of input and output files. Defaults to utf-8.
+
+ """
+
+ encoding = encoding or "utf-8"
+
+ # Read the source
+ if input:
+ if isinstance(input, str):
+ input_file = codecs.open(input, mode="r", encoding=encoding)
+ else:
+ input_file = codecs.getreader(encoding)(input)
+ text = input_file.read()
+ input_file.close()
+ else:
+ text = sys.stdin.read()
+ if not isinstance(text, str): # pragma: no cover
+ text = text.decode(encoding)
+
+ text = text.lstrip('\ufeff') # remove the byte-order mark
+
+ # Convert
+ html = self.convert(text)
+
+ # Write to file or stdout
+ if output:
+ if isinstance(output, str):
+ output_file = codecs.open(output, "w",
+ encoding=encoding,
+ errors="xmlcharrefreplace")
+ output_file.write(html)
+ output_file.close()
+ else:
+ writer = codecs.getwriter(encoding)
+ output_file = writer(output, errors="xmlcharrefreplace")
+ output_file.write(html)
+ # Don't close here. User may want to write more.
+ else:
+ # Encode manually and write bytes to stdout.
+ html = html.encode(encoding, "xmlcharrefreplace")
+ try:
+ # Write bytes directly to buffer (Python 3).
+ sys.stdout.buffer.write(html)
+ except AttributeError: # pragma: no cover
+ # Probably Python 2, which works with bytes by default.
+ sys.stdout.write(html)
+
+ return self
+
+
+"""
+EXPORTED FUNCTIONS
+=============================================================================
+
+Those are the two functions we really mean to export: markdown() and
+markdownFromFile().
+"""
+
+
+def markdown(text, **kwargs):
+ """Convert a markdown string to HTML and return HTML as a unicode string.
+
+ This is a shortcut function for `Markdown` class to cover the most
+ basic use case. It initializes an instance of Markdown, loads the
+ necessary extensions and runs the parser on the given text.
+
+ Keyword arguments:
+
+ * text: Markdown formatted text as Unicode or ASCII string.
+ * Any arguments accepted by the Markdown class.
+
+ Returns: An HTML document as a string.
+
+ """
+ md = Markdown(**kwargs)
+ return md.convert(text)
+
+
+def markdownFromFile(**kwargs):
+ """Read markdown code from a file and write it to a file or a stream.
+
+ This is a shortcut function which initializes an instance of Markdown,
+ and calls the convertFile method rather than convert.
+
+ Keyword arguments:
+
+ * input: a file name or readable object.
+ * output: a file name or writable object.
+ * encoding: Encoding of input and output.
+ * Any arguments accepted by the Markdown class.
+
+ """
+ md = Markdown(**kwargs)
+ md.convertFile(kwargs.get('input', None),
+ kwargs.get('output', None),
+ kwargs.get('encoding', None))
diff --git a/venv/Lib/site-packages/markdown/extensions/__init__.py b/venv/Lib/site-packages/markdown/extensions/__init__.py
new file mode 100644
index 0000000..4bc8e5f
--- /dev/null
+++ b/venv/Lib/site-packages/markdown/extensions/__init__.py
@@ -0,0 +1,107 @@
+"""
+Python Markdown
+
+A Python implementation of John Gruber's Markdown.
+
+Documentation: https://python-markdown.github.io/
+GitHub: https://github.com/Python-Markdown/markdown/
+PyPI: https://pypi.org/project/Markdown/
+
+Started by Manfred Stienstra (http://www.dwerg.net/).
+Maintained for a few years by Yuri Takhteyev (http://www.freewisdom.org).
+Currently maintained by Waylan Limberg (https://github.com/waylan),
+Dmitry Shachnev (https://github.com/mitya57) and Isaac Muse (https://github.com/facelessuser).
+
+Copyright 2007-2018 The Python Markdown Project (v. 1.7 and later)
+Copyright 2004, 2005, 2006 Yuri Takhteyev (v. 0.2-1.6b)
+Copyright 2004 Manfred Stienstra (the original version)
+
+License: BSD (see LICENSE.md for details).
+"""
+
+import warnings
+from ..util import parseBoolValue
+
+
+class Extension:
+ """ Base class for extensions to subclass. """
+
+ # Default config -- to be overriden by a subclass
+ # Must be of the following format:
+ # {
+ # 'key': ['value', 'description']
+ # }
+ # Note that Extension.setConfig will raise a KeyError
+ # if a default is not set here.
+ config = {}
+
+ def __init__(self, **kwargs):
+ """ Initiate Extension and set up configs. """
+ self.setConfigs(kwargs)
+
+ def getConfig(self, key, default=''):
+ """ Return a setting for the given key or an empty string. """
+ if key in self.config:
+ return self.config[key][0]
+ else:
+ return default
+
+ def getConfigs(self):
+ """ Return all configs settings as a dict. """
+ return {key: self.getConfig(key) for key in self.config.keys()}
+
+ def getConfigInfo(self):
+ """ Return all config descriptions as a list of tuples. """
+ return [(key, self.config[key][1]) for key in self.config.keys()]
+
+ def setConfig(self, key, value):
+ """ Set a config setting for `key` with the given `value`. """
+ if isinstance(self.config[key][0], bool):
+ value = parseBoolValue(value)
+ if self.config[key][0] is None:
+ value = parseBoolValue(value, preserve_none=True)
+ self.config[key][0] = value
+
+ def setConfigs(self, items):
+ """ Set multiple config settings given a dict or list of tuples. """
+ if hasattr(items, 'items'):
+ # it's a dict
+ items = items.items()
+ for key, value in items:
+ self.setConfig(key, value)
+
+ def _extendMarkdown(self, *args):
+ """ Private wrapper around extendMarkdown. """
+ md = args[0]
+ try:
+ self.extendMarkdown(md)
+ except TypeError as e:
+ if "missing 1 required positional argument" in str(e):
+ # Must be a 2.x extension. Pass in a dumby md_globals.
+ self.extendMarkdown(md, {})
+ warnings.warn(
+ "The 'md_globals' parameter of '{}.{}.extendMarkdown' is "
+ "deprecated.".format(self.__class__.__module__, self.__class__.__name__),
+ category=DeprecationWarning,
+ stacklevel=2
+ )
+ else:
+ raise
+
+ def extendMarkdown(self, md):
+ """
+ Add the various proccesors and patterns to the Markdown Instance.
+
+ This method must be overriden by every extension.
+
+ Keyword arguments:
+
+ * md: The Markdown instance.
+
+ * md_globals: Global variables in the markdown module namespace.
+
+ """
+ raise NotImplementedError(
+ 'Extension "%s.%s" must define an "extendMarkdown"'
+ 'method.' % (self.__class__.__module__, self.__class__.__name__)
+ )
diff --git a/venv/Lib/site-packages/markdown/extensions/abbr.py b/venv/Lib/site-packages/markdown/extensions/abbr.py
new file mode 100644
index 0000000..9879314
--- /dev/null
+++ b/venv/Lib/site-packages/markdown/extensions/abbr.py
@@ -0,0 +1,99 @@
+'''
+Abbreviation Extension for Python-Markdown
+==========================================
+
+This extension adds abbreviation handling to Python-Markdown.
+
+See
+for documentation.
+
+Oringinal code Copyright 2007-2008 [Waylan Limberg](http://achinghead.com/) and
+ [Seemant Kulleen](http://www.kulleen.org/)
+
+All changes Copyright 2008-2014 The Python Markdown Project
+
+License: [BSD](https://opensource.org/licenses/bsd-license.php)
+
+'''
+
+from . import Extension
+from ..blockprocessors import BlockProcessor
+from ..inlinepatterns import InlineProcessor
+from ..util import AtomicString
+import re
+import xml.etree.ElementTree as etree
+
+
+class AbbrExtension(Extension):
+ """ Abbreviation Extension for Python-Markdown. """
+
+ def extendMarkdown(self, md):
+ """ Insert AbbrPreprocessor before ReferencePreprocessor. """
+ md.parser.blockprocessors.register(AbbrPreprocessor(md.parser), 'abbr', 16)
+
+
+class AbbrPreprocessor(BlockProcessor):
+ """ Abbreviation Preprocessor - parse text for abbr references. """
+
+ RE = re.compile(r'^[*]\[(?P[^\]]*)\][ ]?:[ ]*\n?[ ]*(?P.*)$', re.MULTILINE)
+
+ def test(self, parent, block):
+ return True
+
+ def run(self, parent, blocks):
+ '''
+ Find and remove all Abbreviation references from the text.
+ Each reference is set as a new AbbrPattern in the markdown instance.
+
+ '''
+ block = blocks.pop(0)
+ m = self.RE.search(block)
+ if m:
+ abbr = m.group('abbr').strip()
+ title = m.group('title').strip()
+ self.parser.md.inlinePatterns.register(
+ AbbrInlineProcessor(self._generate_pattern(abbr), title), 'abbr-%s' % abbr, 2
+ )
+ if block[m.end():].strip():
+ # Add any content after match back to blocks as separate block
+ blocks.insert(0, block[m.end():].lstrip('\n'))
+ if block[:m.start()].strip():
+ # Add any content before match back to blocks as separate block
+ blocks.insert(0, block[:m.start()].rstrip('\n'))
+ return True
+ # No match. Restore block.
+ blocks.insert(0, block)
+ return False
+
+ def _generate_pattern(self, text):
+ '''
+ Given a string, returns an regex pattern to match that string.
+
+ 'HTML' -> r'(?P[H][T][M][L])'
+
+ Note: we force each char as a literal match (in brackets) as we don't
+ know what they will be beforehand.
+
+ '''
+ chars = list(text)
+ for i in range(len(chars)):
+ chars[i] = r'[%s]' % chars[i]
+ return r'(?P\b%s\b)' % (r''.join(chars))
+
+
+class AbbrInlineProcessor(InlineProcessor):
+ """ Abbreviation inline pattern. """
+
+ def __init__(self, pattern, title):
+ super().__init__(pattern)
+ self.title = title
+
+ def handleMatch(self, m, data):
+ abbr = etree.Element('abbr')
+ abbr.text = AtomicString(m.group('abbr'))
+ abbr.set('title', self.title)
+ return abbr, m.start(0), m.end(0)
+
+
+def makeExtension(**kwargs): # pragma: no cover
+ return AbbrExtension(**kwargs)
diff --git a/venv/Lib/site-packages/markdown/extensions/admonition.py b/venv/Lib/site-packages/markdown/extensions/admonition.py
new file mode 100644
index 0000000..cb8d901
--- /dev/null
+++ b/venv/Lib/site-packages/markdown/extensions/admonition.py
@@ -0,0 +1,170 @@
+"""
+Admonition extension for Python-Markdown
+========================================
+
+Adds rST-style admonitions. Inspired by [rST][] feature with the same name.
+
+[rST]: http://docutils.sourceforge.net/docs/ref/rst/directives.html#specific-admonitions # noqa
+
+See
+for documentation.
+
+Original code Copyright [Tiago Serafim](https://www.tiagoserafim.com/).
+
+All changes Copyright The Python Markdown Project
+
+License: [BSD](https://opensource.org/licenses/bsd-license.php)
+
+"""
+
+from . import Extension
+from ..blockprocessors import BlockProcessor
+import xml.etree.ElementTree as etree
+import re
+
+
+class AdmonitionExtension(Extension):
+ """ Admonition extension for Python-Markdown. """
+
+ def extendMarkdown(self, md):
+ """ Add Admonition to Markdown instance. """
+ md.registerExtension(self)
+
+ md.parser.blockprocessors.register(AdmonitionProcessor(md.parser), 'admonition', 105)
+
+
+class AdmonitionProcessor(BlockProcessor):
+
+ CLASSNAME = 'admonition'
+ CLASSNAME_TITLE = 'admonition-title'
+ RE = re.compile(r'(?:^|\n)!!! ?([\w\-]+(?: +[\w\-]+)*)(?: +"(.*?)")? *(?:\n|$)')
+ RE_SPACES = re.compile(' +')
+
+ def __init__(self, parser):
+ """Initialization."""
+
+ super().__init__(parser)
+
+ self.current_sibling = None
+ self.content_indention = 0
+
+ def parse_content(self, parent, block):
+ """Get sibling admonition.
+
+ Retrieve the appropriate sibling element. This can get tricky when
+ dealing with lists.
+
+ """
+
+ old_block = block
+ the_rest = ''
+
+ # We already acquired the block via test
+ if self.current_sibling is not None:
+ sibling = self.current_sibling
+ block, the_rest = self.detab(block, self.content_indent)
+ self.current_sibling = None
+ self.content_indent = 0
+ return sibling, block, the_rest
+
+ sibling = self.lastChild(parent)
+
+ if sibling is None or sibling.get('class', '').find(self.CLASSNAME) == -1:
+ sibling = None
+ else:
+ # If the last child is a list and the content is sufficiently indented
+ # to be under it, then the content's sibling is in the list.
+ last_child = self.lastChild(sibling)
+ indent = 0
+ while last_child:
+ if (
+ sibling and block.startswith(' ' * self.tab_length * 2) and
+ last_child and last_child.tag in ('ul', 'ol', 'dl')
+ ):
+
+ # The expectation is that we'll find an
or
.
+ # We should get its last child as well.
+ sibling = self.lastChild(last_child)
+ last_child = self.lastChild(sibling) if sibling else None
+
+ # Context has been lost at this point, so we must adjust the
+ # text's indentation level so it will be evaluated correctly
+ # under the list.
+ block = block[self.tab_length:]
+ indent += self.tab_length
+ else:
+ last_child = None
+
+ if not block.startswith(' ' * self.tab_length):
+ sibling = None
+
+ if sibling is not None:
+ indent += self.tab_length
+ block, the_rest = self.detab(old_block, indent)
+ self.current_sibling = sibling
+ self.content_indent = indent
+
+ return sibling, block, the_rest
+
+ def test(self, parent, block):
+
+ if self.RE.search(block):
+ return True
+ else:
+ return self.parse_content(parent, block)[0] is not None
+
+ def run(self, parent, blocks):
+ block = blocks.pop(0)
+ m = self.RE.search(block)
+
+ if m:
+ if m.start() > 0:
+ self.parser.parseBlocks(parent, [block[:m.start()]])
+ block = block[m.end():] # removes the first line
+ block, theRest = self.detab(block)
+ else:
+ sibling, block, theRest = self.parse_content(parent, block)
+
+ if m:
+ klass, title = self.get_class_and_title(m)
+ div = etree.SubElement(parent, 'div')
+ div.set('class', '{} {}'.format(self.CLASSNAME, klass))
+ if title:
+ p = etree.SubElement(div, 'p')
+ p.text = title
+ p.set('class', self.CLASSNAME_TITLE)
+ else:
+ # Sibling is a list item, but we need to wrap it's content should be wrapped in
+ if sibling.tag in ('li', 'dd') and sibling.text:
+ text = sibling.text
+ sibling.text = ''
+ p = etree.SubElement(sibling, 'p')
+ p.text = text
+
+ div = sibling
+
+ self.parser.parseChunk(div, block)
+
+ if theRest:
+ # This block contained unindented line(s) after the first indented
+ # line. Insert these lines as the first block of the master blocks
+ # list for future processing.
+ blocks.insert(0, theRest)
+
+ def get_class_and_title(self, match):
+ klass, title = match.group(1).lower(), match.group(2)
+ klass = self.RE_SPACES.sub(' ', klass)
+ if title is None:
+ # no title was provided, use the capitalized classname as title
+ # e.g.: `!!! note` will render
+ # `
Note
`
+ title = klass.split(' ', 1)[0].capitalize()
+ elif title == '':
+ # an explicit blank title should not be rendered
+ # e.g.: `!!! warning ""` will *not* render `p` with a title
+ title = None
+ return klass, title
+
+
+def makeExtension(**kwargs): # pragma: no cover
+ return AdmonitionExtension(**kwargs)
diff --git a/venv/Lib/site-packages/markdown/extensions/attr_list.py b/venv/Lib/site-packages/markdown/extensions/attr_list.py
new file mode 100644
index 0000000..9a67551
--- /dev/null
+++ b/venv/Lib/site-packages/markdown/extensions/attr_list.py
@@ -0,0 +1,166 @@
+"""
+Attribute List Extension for Python-Markdown
+============================================
+
+Adds attribute list syntax. Inspired by
+[maruku](http://maruku.rubyforge.org/proposal.html#attribute_lists)'s
+feature of the same name.
+
+See
+for documentation.
+
+Original code Copyright 2011 [Waylan Limberg](http://achinghead.com/).
+
+All changes Copyright 2011-2014 The Python Markdown Project
+
+License: [BSD](https://opensource.org/licenses/bsd-license.php)
+
+"""
+
+from . import Extension
+from ..treeprocessors import Treeprocessor
+import re
+
+
+def _handle_double_quote(s, t):
+ k, v = t.split('=', 1)
+ return k, v.strip('"')
+
+
+def _handle_single_quote(s, t):
+ k, v = t.split('=', 1)
+ return k, v.strip("'")
+
+
+def _handle_key_value(s, t):
+ return t.split('=', 1)
+
+
+def _handle_word(s, t):
+ if t.startswith('.'):
+ return '.', t[1:]
+ if t.startswith('#'):
+ return 'id', t[1:]
+ return t, t
+
+
+_scanner = re.Scanner([
+ (r'[^ =]+=".*?"', _handle_double_quote),
+ (r"[^ =]+='.*?'", _handle_single_quote),
+ (r'[^ =]+=[^ =]+', _handle_key_value),
+ (r'[^ =]+', _handle_word),
+ (r' ', None)
+])
+
+
+def get_attrs(str):
+ """ Parse attribute list and return a list of attribute tuples. """
+ return _scanner.scan(str)[0]
+
+
+def isheader(elem):
+ return elem.tag in ['h1', 'h2', 'h3', 'h4', 'h5', 'h6']
+
+
+class AttrListTreeprocessor(Treeprocessor):
+
+ BASE_RE = r'\{\:?[ ]*([^\}\n ][^\}\n]*)[ ]*\}'
+ HEADER_RE = re.compile(r'[ ]+{}[ ]*$'.format(BASE_RE))
+ BLOCK_RE = re.compile(r'\n[ ]*{}[ ]*$'.format(BASE_RE))
+ INLINE_RE = re.compile(r'^{}'.format(BASE_RE))
+ NAME_RE = re.compile(r'[^A-Z_a-z\u00c0-\u00d6\u00d8-\u00f6\u00f8-\u02ff'
+ r'\u0370-\u037d\u037f-\u1fff\u200c-\u200d'
+ r'\u2070-\u218f\u2c00-\u2fef\u3001-\ud7ff'
+ r'\uf900-\ufdcf\ufdf0-\ufffd'
+ r'\:\-\.0-9\u00b7\u0300-\u036f\u203f-\u2040]+')
+
+ def run(self, doc):
+ for elem in doc.iter():
+ if self.md.is_block_level(elem.tag):
+ # Block level: check for attrs on last line of text
+ RE = self.BLOCK_RE
+ if isheader(elem) or elem.tag in ['dt', 'td', 'th']:
+ # header, def-term, or table cell: check for attrs at end of element
+ RE = self.HEADER_RE
+ if len(elem) and elem.tag == 'li':
+ # special case list items. children may include a ul or ol.
+ pos = None
+ # find the ul or ol position
+ for i, child in enumerate(elem):
+ if child.tag in ['ul', 'ol']:
+ pos = i
+ break
+ if pos is None and elem[-1].tail:
+ # use tail of last child. no ul or ol.
+ m = RE.search(elem[-1].tail)
+ if m:
+ self.assign_attrs(elem, m.group(1))
+ elem[-1].tail = elem[-1].tail[:m.start()]
+ elif pos is not None and pos > 0 and elem[pos-1].tail:
+ # use tail of last child before ul or ol
+ m = RE.search(elem[pos-1].tail)
+ if m:
+ self.assign_attrs(elem, m.group(1))
+ elem[pos-1].tail = elem[pos-1].tail[:m.start()]
+ elif elem.text:
+ # use text. ul is first child.
+ m = RE.search(elem.text)
+ if m:
+ self.assign_attrs(elem, m.group(1))
+ elem.text = elem.text[:m.start()]
+ elif len(elem) and elem[-1].tail:
+ # has children. Get from tail of last child
+ m = RE.search(elem[-1].tail)
+ if m:
+ self.assign_attrs(elem, m.group(1))
+ elem[-1].tail = elem[-1].tail[:m.start()]
+ if isheader(elem):
+ # clean up trailing #s
+ elem[-1].tail = elem[-1].tail.rstrip('#').rstrip()
+ elif elem.text:
+ # no children. Get from text.
+ m = RE.search(elem.text)
+ if m:
+ self.assign_attrs(elem, m.group(1))
+ elem.text = elem.text[:m.start()]
+ if isheader(elem):
+ # clean up trailing #s
+ elem.text = elem.text.rstrip('#').rstrip()
+ else:
+ # inline: check for attrs at start of tail
+ if elem.tail:
+ m = self.INLINE_RE.match(elem.tail)
+ if m:
+ self.assign_attrs(elem, m.group(1))
+ elem.tail = elem.tail[m.end():]
+
+ def assign_attrs(self, elem, attrs):
+ """ Assign attrs to element. """
+ for k, v in get_attrs(attrs):
+ if k == '.':
+ # add to class
+ cls = elem.get('class')
+ if cls:
+ elem.set('class', '{} {}'.format(cls, v))
+ else:
+ elem.set('class', v)
+ else:
+ # assign attr k with v
+ elem.set(self.sanitize_name(k), v)
+
+ def sanitize_name(self, name):
+ """
+ Sanitize name as 'an XML Name, minus the ":"'.
+ See https://www.w3.org/TR/REC-xml-names/#NT-NCName
+ """
+ return self.NAME_RE.sub('_', name)
+
+
+class AttrListExtension(Extension):
+ def extendMarkdown(self, md):
+ md.treeprocessors.register(AttrListTreeprocessor(md), 'attr_list', 8)
+ md.registerExtension(self)
+
+
+def makeExtension(**kwargs): # pragma: no cover
+ return AttrListExtension(**kwargs)
diff --git a/venv/Lib/site-packages/markdown/extensions/codehilite.py b/venv/Lib/site-packages/markdown/extensions/codehilite.py
new file mode 100644
index 0000000..e1c2218
--- /dev/null
+++ b/venv/Lib/site-packages/markdown/extensions/codehilite.py
@@ -0,0 +1,307 @@
+"""
+CodeHilite Extension for Python-Markdown
+========================================
+
+Adds code/syntax highlighting to standard Python-Markdown code blocks.
+
+See
+for documentation.
+
+Original code Copyright 2006-2008 [Waylan Limberg](http://achinghead.com/).
+
+All changes Copyright 2008-2014 The Python Markdown Project
+
+License: [BSD](https://opensource.org/licenses/bsd-license.php)
+
+"""
+
+from . import Extension
+from ..treeprocessors import Treeprocessor
+from ..util import parseBoolValue
+
+try: # pragma: no cover
+ from pygments import highlight
+ from pygments.lexers import get_lexer_by_name, guess_lexer
+ from pygments.formatters import get_formatter_by_name
+ pygments = True
+except ImportError: # pragma: no cover
+ pygments = False
+
+
+def parse_hl_lines(expr):
+ """Support our syntax for emphasizing certain lines of code.
+
+ expr should be like '1 2' to emphasize lines 1 and 2 of a code block.
+ Returns a list of ints, the line numbers to emphasize.
+ """
+ if not expr:
+ return []
+
+ try:
+ return list(map(int, expr.split()))
+ except ValueError: # pragma: no cover
+ return []
+
+
+# ------------------ The Main CodeHilite Class ----------------------
+class CodeHilite:
+ """
+ Determine language of source code, and pass it on to the Pygments highlighter.
+
+ Usage:
+ code = CodeHilite(src=some_code, lang='python')
+ html = code.hilite()
+
+ Arguments:
+ * src: Source string or any object with a .readline attribute.
+
+ * lang: String name of Pygments lexer to use for highlighting. Default: `None`.
+
+ * guess_lang: Auto-detect which lexer to use. Ignored if `lang` is set to a valid
+ value. Default: `True`.
+
+ * use_pygments: Pass code to pygments for code highlighting. If `False`, the code is
+ instead wrapped for highlighting by a JavaScript library. Default: `True`.
+
+ * linenums: An alias to Pygments `linenos` formatter option. Default: `None`.
+
+ * css_class: An alias to Pygments `cssclass` formatter option. Default: 'codehilite'.
+
+ * lang_prefix: Prefix prepended to the language when `use_pygments` is `False`.
+ Default: "language-".
+
+ Other Options:
+ Any other options are accepted and passed on to the lexer and formatter. Therefore,
+ valid options include any options which are accepted by the `html` formatter or
+ whichever lexer the code's language uses. Note that most lexers do not have any
+ options. However, a few have very useful options, such as PHP's `startinline` option.
+ Any invalid options are ignored without error.
+
+ Formatter options: https://pygments.org/docs/formatters/#HtmlFormatter
+ Lexer Options: https://pygments.org/docs/lexers/
+
+ Advanced Usage:
+ code = CodeHilite(
+ src = some_code,
+ lang = 'php',
+ startinline = True, # Lexer option. Snippet does not start with `).
+
+ returns : A string of html.
+
+ """
+
+ self.src = self.src.strip('\n')
+
+ if self.lang is None and shebang:
+ self._parseHeader()
+
+ if pygments and self.use_pygments:
+ try:
+ lexer = get_lexer_by_name(self.lang, **self.options)
+ except ValueError:
+ try:
+ if self.guess_lang:
+ lexer = guess_lexer(self.src, **self.options)
+ else:
+ lexer = get_lexer_by_name('text', **self.options)
+ except ValueError: # pragma: no cover
+ lexer = get_lexer_by_name('text', **self.options)
+ formatter = get_formatter_by_name('html', **self.options)
+ return highlight(self.src, lexer, formatter)
+ else:
+ # just escape and build markup usable by JS highlighting libs
+ txt = self.src.replace('&', '&')
+ txt = txt.replace('<', '<')
+ txt = txt.replace('>', '>')
+ txt = txt.replace('"', '"')
+ classes = []
+ if self.lang:
+ classes.append('{}{}'.format(self.lang_prefix, self.lang))
+ if self.options['linenos']:
+ classes.append('linenums')
+ class_str = ''
+ if classes:
+ class_str = ' class="{}"'.format(' '.join(classes))
+ return '
{}\n
\n'.format(
+ self.options['cssclass'],
+ class_str,
+ txt
+ )
+
+ def _parseHeader(self):
+ """
+ Determines language of a code block from shebang line and whether the
+ said line should be removed or left in place. If the sheband line
+ contains a path (even a single /) then it is assumed to be a real
+ shebang line and left alone. However, if no path is given
+ (e.i.: #!python or :::python) then it is assumed to be a mock shebang
+ for language identification of a code fragment and removed from the
+ code block prior to processing for code highlighting. When a mock
+ shebang (e.i: #!python) is found, line numbering is turned on. When
+ colons are found in place of a shebang (e.i.: :::python), line
+ numbering is left in the current state - off by default.
+
+ Also parses optional list of highlight lines, like:
+
+ :::python hl_lines="1 3"
+ """
+
+ import re
+
+ # split text into lines
+ lines = self.src.split("\n")
+ # pull first line to examine
+ fl = lines.pop(0)
+
+ c = re.compile(r'''
+ (?:(?:^::+)|(?P^[#]!)) # Shebang or 2 or more colons
+ (?P(?:/\w+)*[/ ])? # Zero or 1 path
+ (?P[\w#.+-]*) # The language
+ \s* # Arbitrary whitespace
+ # Optional highlight lines, single- or double-quote-delimited
+ (hl_lines=(?P"|')(?P.*?)(?P=quot))?
+ ''', re.VERBOSE)
+ # search first line for shebang
+ m = c.search(fl)
+ if m:
+ # we have a match
+ try:
+ self.lang = m.group('lang').lower()
+ except IndexError: # pragma: no cover
+ self.lang = None
+ if m.group('path'):
+ # path exists - restore first line
+ lines.insert(0, fl)
+ if self.options['linenos'] is None and m.group('shebang'):
+ # Overridable and Shebang exists - use line numbers
+ self.options['linenos'] = True
+
+ self.options['hl_lines'] = parse_hl_lines(m.group('hl_lines'))
+ else:
+ # No match
+ lines.insert(0, fl)
+
+ self.src = "\n".join(lines).strip("\n")
+
+
+# ------------------ The Markdown Extension -------------------------------
+
+
+class HiliteTreeprocessor(Treeprocessor):
+ """ Hilight source code in code blocks. """
+
+ def code_unescape(self, text):
+ """Unescape code."""
+ text = text.replace("<", "<")
+ text = text.replace(">", ">")
+ # Escaped '&' should be replaced at the end to avoid
+ # conflicting with < and >.
+ text = text.replace("&", "&")
+ return text
+
+ def run(self, root):
+ """ Find code blocks and store in htmlStash. """
+ blocks = root.iter('pre')
+ for block in blocks:
+ if len(block) == 1 and block[0].tag == 'code':
+ code = CodeHilite(
+ self.code_unescape(block[0].text),
+ tab_length=self.md.tab_length,
+ style=self.config.pop('pygments_style', 'default'),
+ **self.config
+ )
+ placeholder = self.md.htmlStash.store(code.hilite())
+ # Clear codeblock in etree instance
+ block.clear()
+ # Change to p element which will later
+ # be removed when inserting raw html
+ block.tag = 'p'
+ block.text = placeholder
+
+
+class CodeHiliteExtension(Extension):
+ """ Add source code hilighting to markdown codeblocks. """
+
+ def __init__(self, **kwargs):
+ # define default configs
+ self.config = {
+ 'linenums': [None,
+ "Use lines numbers. True|table|inline=yes, False=no, None=auto"],
+ 'guess_lang': [True,
+ "Automatic language detection - Default: True"],
+ 'css_class': ["codehilite",
+ "Set class name for wrapper
- "
+ "Default: codehilite"],
+ 'pygments_style': ['default',
+ 'Pygments HTML Formatter Style '
+ '(Colorscheme) - Default: default'],
+ 'noclasses': [False,
+ 'Use inline styles instead of CSS classes - '
+ 'Default false'],
+ 'use_pygments': [True,
+ 'Use Pygments to Highlight code blocks. '
+ 'Disable if using a JavaScript library. '
+ 'Default: True'],
+ 'lang_prefix': [
+ 'language-',
+ 'Prefix prepended to the language when use_pygments is false. Default: "language-"'
+ ]
+ }
+
+ for key, value in kwargs.items():
+ if key in self.config:
+ self.setConfig(key, value)
+ else:
+ # manually set unknown keywords.
+ if isinstance(value, str):
+ try:
+ # Attempt to parse str as a bool value
+ value = parseBoolValue(value, preserve_none=True)
+ except ValueError:
+ pass # Assume it's not a bool value. Use as-is.
+ self.config[key] = [value, '']
+
+ def extendMarkdown(self, md):
+ """ Add HilitePostprocessor to Markdown instance. """
+ hiliter = HiliteTreeprocessor(md)
+ hiliter.config = self.getConfigs()
+ md.treeprocessors.register(hiliter, 'hilite', 30)
+
+ md.registerExtension(self)
+
+
+def makeExtension(**kwargs): # pragma: no cover
+ return CodeHiliteExtension(**kwargs)
diff --git a/venv/Lib/site-packages/markdown/extensions/def_list.py b/venv/Lib/site-packages/markdown/extensions/def_list.py
new file mode 100644
index 0000000..0e8e452
--- /dev/null
+++ b/venv/Lib/site-packages/markdown/extensions/def_list.py
@@ -0,0 +1,111 @@
+"""
+Definition List Extension for Python-Markdown
+=============================================
+
+Adds parsing of Definition Lists to Python-Markdown.
+
+See
+for documentation.
+
+Original code Copyright 2008 [Waylan Limberg](http://achinghead.com)
+
+All changes Copyright 2008-2014 The Python Markdown Project
+
+License: [BSD](https://opensource.org/licenses/bsd-license.php)
+
+"""
+
+from . import Extension
+from ..blockprocessors import BlockProcessor, ListIndentProcessor
+import xml.etree.ElementTree as etree
+import re
+
+
+class DefListProcessor(BlockProcessor):
+ """ Process Definition Lists. """
+
+ RE = re.compile(r'(^|\n)[ ]{0,3}:[ ]{1,3}(.*?)(\n|$)')
+ NO_INDENT_RE = re.compile(r'^[ ]{0,3}[^ :]')
+
+ def test(self, parent, block):
+ return bool(self.RE.search(block))
+
+ def run(self, parent, blocks):
+
+ raw_block = blocks.pop(0)
+ m = self.RE.search(raw_block)
+ terms = [term.strip() for term in
+ raw_block[:m.start()].split('\n') if term.strip()]
+ block = raw_block[m.end():]
+ no_indent = self.NO_INDENT_RE.match(block)
+ if no_indent:
+ d, theRest = (block, None)
+ else:
+ d, theRest = self.detab(block)
+ if d:
+ d = '{}\n{}'.format(m.group(2), d)
+ else:
+ d = m.group(2)
+ sibling = self.lastChild(parent)
+ if not terms and sibling is None:
+ # This is not a definition item. Most likely a paragraph that
+ # starts with a colon at the beginning of a document or list.
+ blocks.insert(0, raw_block)
+ return False
+ if not terms and sibling.tag == 'p':
+ # The previous paragraph contains the terms
+ state = 'looselist'
+ terms = sibling.text.split('\n')
+ parent.remove(sibling)
+ # Acquire new sibling
+ sibling = self.lastChild(parent)
+ else:
+ state = 'list'
+
+ if sibling is not None and sibling.tag == 'dl':
+ # This is another item on an existing list
+ dl = sibling
+ if not terms and len(dl) and dl[-1].tag == 'dd' and len(dl[-1]):
+ state = 'looselist'
+ else:
+ # This is a new list
+ dl = etree.SubElement(parent, 'dl')
+ # Add terms
+ for term in terms:
+ dt = etree.SubElement(dl, 'dt')
+ dt.text = term
+ # Add definition
+ self.parser.state.set(state)
+ dd = etree.SubElement(dl, 'dd')
+ self.parser.parseBlocks(dd, [d])
+ self.parser.state.reset()
+
+ if theRest:
+ blocks.insert(0, theRest)
+
+
+class DefListIndentProcessor(ListIndentProcessor):
+ """ Process indented children of definition list items. """
+
+ # Defintion lists need to be aware of all list types
+ ITEM_TYPES = ['dd', 'li']
+ LIST_TYPES = ['dl', 'ol', 'ul']
+
+ def create_item(self, parent, block):
+ """ Create a new dd or li (depending on parent) and parse the block with it as the parent. """
+
+ dd = etree.SubElement(parent, 'dd')
+ self.parser.parseBlocks(dd, [block])
+
+
+class DefListExtension(Extension):
+ """ Add definition lists to Markdown. """
+
+ def extendMarkdown(self, md):
+ """ Add an instance of DefListProcessor to BlockParser. """
+ md.parser.blockprocessors.register(DefListIndentProcessor(md.parser), 'defindent', 85)
+ md.parser.blockprocessors.register(DefListProcessor(md.parser), 'deflist', 25)
+
+
+def makeExtension(**kwargs): # pragma: no cover
+ return DefListExtension(**kwargs)
diff --git a/venv/Lib/site-packages/markdown/extensions/extra.py b/venv/Lib/site-packages/markdown/extensions/extra.py
new file mode 100644
index 0000000..ebd168c
--- /dev/null
+++ b/venv/Lib/site-packages/markdown/extensions/extra.py
@@ -0,0 +1,58 @@
+"""
+Python-Markdown Extra Extension
+===============================
+
+A compilation of various Python-Markdown extensions that imitates
+[PHP Markdown Extra](http://michelf.com/projects/php-markdown/extra/).
+
+Note that each of the individual extensions still need to be available
+on your PYTHONPATH. This extension simply wraps them all up as a
+convenience so that only one extension needs to be listed when
+initiating Markdown. See the documentation for each individual
+extension for specifics about that extension.
+
+There may be additional extensions that are distributed with
+Python-Markdown that are not included here in Extra. Those extensions
+are not part of PHP Markdown Extra, and therefore, not part of
+Python-Markdown Extra. If you really would like Extra to include
+additional extensions, we suggest creating your own clone of Extra
+under a differant name. You could also edit the `extensions` global
+variable defined below, but be aware that such changes may be lost
+when you upgrade to any future version of Python-Markdown.
+
+See
+for documentation.
+
+Copyright The Python Markdown Project
+
+License: [BSD](https://opensource.org/licenses/bsd-license.php)
+
+"""
+
+from . import Extension
+
+extensions = [
+ 'fenced_code',
+ 'footnotes',
+ 'attr_list',
+ 'def_list',
+ 'tables',
+ 'abbr',
+ 'md_in_html'
+]
+
+
+class ExtraExtension(Extension):
+ """ Add various extensions to Markdown class."""
+
+ def __init__(self, **kwargs):
+ """ config is a dumb holder which gets passed to actual ext later. """
+ self.config = kwargs
+
+ def extendMarkdown(self, md):
+ """ Register extension instances. """
+ md.registerExtensions(extensions, self.config)
+
+
+def makeExtension(**kwargs): # pragma: no cover
+ return ExtraExtension(**kwargs)
diff --git a/venv/Lib/site-packages/markdown/extensions/fenced_code.py b/venv/Lib/site-packages/markdown/extensions/fenced_code.py
new file mode 100644
index 0000000..9be0ca0
--- /dev/null
+++ b/venv/Lib/site-packages/markdown/extensions/fenced_code.py
@@ -0,0 +1,179 @@
+"""
+Fenced Code Extension for Python Markdown
+=========================================
+
+This extension adds Fenced Code Blocks to Python-Markdown.
+
+See
+for documentation.
+
+Original code Copyright 2007-2008 [Waylan Limberg](http://achinghead.com/).
+
+
+All changes Copyright 2008-2014 The Python Markdown Project
+
+License: [BSD](https://opensource.org/licenses/bsd-license.php)
+"""
+
+
+from textwrap import dedent
+from . import Extension
+from ..preprocessors import Preprocessor
+from .codehilite import CodeHilite, CodeHiliteExtension, parse_hl_lines
+from .attr_list import get_attrs, AttrListExtension
+from ..util import parseBoolValue
+import re
+
+
+class FencedCodeExtension(Extension):
+ def __init__(self, **kwargs):
+ self.config = {
+ 'lang_prefix': ['language-', 'Prefix prepended to the language. Default: "language-"']
+ }
+ super().__init__(**kwargs)
+
+ def extendMarkdown(self, md):
+ """ Add FencedBlockPreprocessor to the Markdown instance. """
+ md.registerExtension(self)
+
+ md.preprocessors.register(FencedBlockPreprocessor(md, self.getConfigs()), 'fenced_code_block', 25)
+
+
+class FencedBlockPreprocessor(Preprocessor):
+ FENCED_BLOCK_RE = re.compile(
+ dedent(r'''
+ (?P^(?:~{3,}|`{3,}))[ ]* # opening fence
+ ((\{(?P[^\}\n]*)\})| # (optional {attrs} or
+ (\.?(?P[\w#.+-]*)[ ]*)? # optional (.)lang
+ (hl_lines=(?P"|')(?P.*?)(?P=quot)[ ]*)?) # optional hl_lines)
+ \n # newline (end of opening fence)
+ (?P.*?)(?<=\n) # the code block
+ (?P=fence)[ ]*$ # closing fence
+ '''),
+ re.MULTILINE | re.DOTALL | re.VERBOSE
+ )
+
+ def __init__(self, md, config):
+ super().__init__(md)
+ self.config = config
+ self.checked_for_deps = False
+ self.codehilite_conf = {}
+ self.use_attr_list = False
+ # List of options to convert to bool values
+ self.bool_options = [
+ 'linenums',
+ 'guess_lang',
+ 'noclasses',
+ 'use_pygments'
+ ]
+
+ def run(self, lines):
+ """ Match and store Fenced Code Blocks in the HtmlStash. """
+
+ # Check for dependent extensions
+ if not self.checked_for_deps:
+ for ext in self.md.registeredExtensions:
+ if isinstance(ext, CodeHiliteExtension):
+ self.codehilite_conf = ext.getConfigs()
+ if isinstance(ext, AttrListExtension):
+ self.use_attr_list = True
+
+ self.checked_for_deps = True
+
+ text = "\n".join(lines)
+ while 1:
+ m = self.FENCED_BLOCK_RE.search(text)
+ if m:
+ lang, id, classes, config = None, '', [], {}
+ if m.group('attrs'):
+ id, classes, config = self.handle_attrs(get_attrs(m.group('attrs')))
+ if len(classes):
+ lang = classes.pop(0)
+ else:
+ if m.group('lang'):
+ lang = m.group('lang')
+ if m.group('hl_lines'):
+ # Support hl_lines outside of attrs for backward-compatibility
+ config['hl_lines'] = parse_hl_lines(m.group('hl_lines'))
+
+ # If config is not empty, then the codehighlite extension
+ # is enabled, so we call it to highlight the code
+ if self.codehilite_conf and self.codehilite_conf['use_pygments'] and config.get('use_pygments', True):
+ local_config = self.codehilite_conf.copy()
+ local_config.update(config)
+ # Combine classes with cssclass. Ensure cssclass is at end
+ # as pygments appends a suffix under certain circumstances.
+ # Ignore ID as Pygments does not offer an option to set it.
+ if classes:
+ local_config['css_class'] = '{} {}'.format(
+ ' '.join(classes),
+ local_config['css_class']
+ )
+ highliter = CodeHilite(
+ m.group('code'),
+ lang=lang,
+ style=local_config.pop('pygments_style', 'default'),
+ **local_config
+ )
+
+ code = highliter.hilite(shebang=False)
+ else:
+ id_attr = lang_attr = class_attr = kv_pairs = ''
+ if lang:
+ lang_attr = ' class="{}{}"'.format(self.config.get('lang_prefix', 'language-'), lang)
+ if classes:
+ class_attr = ' class="{}"'.format(' '.join(classes))
+ if id:
+ id_attr = ' id="{}"'.format(id)
+ if self.use_attr_list and config and not config.get('use_pygments', False):
+ # Only assign key/value pairs to code element if attr_list ext is enabled, key/value pairs
+ # were defined on the code block, and the `use_pygments` key was not set to True. The
+ # `use_pygments` key could be either set to False or not defined. It is omitted from output.
+ kv_pairs = ' ' + ' '.join(
+ '{k}="{v}"'.format(k=k, v=v) for k, v in config.items() if k != 'use_pygments'
+ )
+ code = '
{code}
'.format(
+ id=id_attr,
+ cls=class_attr,
+ lang=lang_attr,
+ kv=kv_pairs,
+ code=self._escape(m.group('code'))
+ )
+
+ placeholder = self.md.htmlStash.store(code)
+ text = '{}\n{}\n{}'.format(text[:m.start()],
+ placeholder,
+ text[m.end():])
+ else:
+ break
+ return text.split("\n")
+
+ def handle_attrs(self, attrs):
+ """ Return tuple: (id, [list, of, classes], {configs}) """
+ id = ''
+ classes = []
+ configs = {}
+ for k, v in attrs:
+ if k == 'id':
+ id = v
+ elif k == '.':
+ classes.append(v)
+ elif k == 'hl_lines':
+ configs[k] = parse_hl_lines(v)
+ elif k in self.bool_options:
+ configs[k] = parseBoolValue(v, fail_on_errors=False, preserve_none=True)
+ else:
+ configs[k] = v
+ return id, classes, configs
+
+ def _escape(self, txt):
+ """ basic html escaping """
+ txt = txt.replace('&', '&')
+ txt = txt.replace('<', '<')
+ txt = txt.replace('>', '>')
+ txt = txt.replace('"', '"')
+ return txt
+
+
+def makeExtension(**kwargs): # pragma: no cover
+ return FencedCodeExtension(**kwargs)
diff --git a/venv/Lib/site-packages/markdown/extensions/footnotes.py b/venv/Lib/site-packages/markdown/extensions/footnotes.py
new file mode 100644
index 0000000..f6f4c85
--- /dev/null
+++ b/venv/Lib/site-packages/markdown/extensions/footnotes.py
@@ -0,0 +1,402 @@
+"""
+Footnotes Extension for Python-Markdown
+=======================================
+
+Adds footnote handling to Python-Markdown.
+
+See
+for documentation.
+
+Copyright The Python Markdown Project
+
+License: [BSD](https://opensource.org/licenses/bsd-license.php)
+
+"""
+
+from . import Extension
+from ..blockprocessors import BlockProcessor
+from ..inlinepatterns import InlineProcessor
+from ..treeprocessors import Treeprocessor
+from ..postprocessors import Postprocessor
+from .. import util
+from collections import OrderedDict
+import re
+import copy
+import xml.etree.ElementTree as etree
+
+FN_BACKLINK_TEXT = util.STX + "zz1337820767766393qq" + util.ETX
+NBSP_PLACEHOLDER = util.STX + "qq3936677670287331zz" + util.ETX
+RE_REF_ID = re.compile(r'(fnref)(\d+)')
+
+
+class FootnoteExtension(Extension):
+ """ Footnote Extension. """
+
+ def __init__(self, **kwargs):
+ """ Setup configs. """
+
+ self.config = {
+ 'PLACE_MARKER':
+ ["///Footnotes Go Here///",
+ "The text string that marks where the footnotes go"],
+ 'UNIQUE_IDS':
+ [False,
+ "Avoid name collisions across "
+ "multiple calls to reset()."],
+ "BACKLINK_TEXT":
+ ["↩",
+ "The text string that links from the footnote "
+ "to the reader's place."],
+ "BACKLINK_TITLE":
+ ["Jump back to footnote %d in the text",
+ "The text string used for the title HTML attribute "
+ "of the backlink. %d will be replaced by the "
+ "footnote number."],
+ "SEPARATOR":
+ [":",
+ "Footnote separator."]
+ }
+ super().__init__(**kwargs)
+
+ # In multiple invocations, emit links that don't get tangled.
+ self.unique_prefix = 0
+ self.found_refs = {}
+ self.used_refs = set()
+
+ self.reset()
+
+ def extendMarkdown(self, md):
+ """ Add pieces to Markdown. """
+ md.registerExtension(self)
+ self.parser = md.parser
+ self.md = md
+ # Insert a blockprocessor before ReferencePreprocessor
+ md.parser.blockprocessors.register(FootnoteBlockProcessor(self), 'footnote', 17)
+
+ # Insert an inline pattern before ImageReferencePattern
+ FOOTNOTE_RE = r'\[\^([^\]]*)\]' # blah blah [^1] blah
+ md.inlinePatterns.register(FootnoteInlineProcessor(FOOTNOTE_RE, self), 'footnote', 175)
+ # Insert a tree-processor that would actually add the footnote div
+ # This must be before all other treeprocessors (i.e., inline and
+ # codehilite) so they can run on the the contents of the div.
+ md.treeprocessors.register(FootnoteTreeprocessor(self), 'footnote', 50)
+
+ # Insert a tree-processor that will run after inline is done.
+ # In this tree-processor we want to check our duplicate footnote tracker
+ # And add additional backrefs to the footnote pointing back to the
+ # duplicated references.
+ md.treeprocessors.register(FootnotePostTreeprocessor(self), 'footnote-duplicate', 15)
+
+ # Insert a postprocessor after amp_substitute processor
+ md.postprocessors.register(FootnotePostprocessor(self), 'footnote', 25)
+
+ def reset(self):
+ """ Clear footnotes on reset, and prepare for distinct document. """
+ self.footnotes = OrderedDict()
+ self.unique_prefix += 1
+ self.found_refs = {}
+ self.used_refs = set()
+
+ def unique_ref(self, reference, found=False):
+ """ Get a unique reference if there are duplicates. """
+ if not found:
+ return reference
+
+ original_ref = reference
+ while reference in self.used_refs:
+ ref, rest = reference.split(self.get_separator(), 1)
+ m = RE_REF_ID.match(ref)
+ if m:
+ reference = '%s%d%s%s' % (m.group(1), int(m.group(2))+1, self.get_separator(), rest)
+ else:
+ reference = '%s%d%s%s' % (ref, 2, self.get_separator(), rest)
+
+ self.used_refs.add(reference)
+ if original_ref in self.found_refs:
+ self.found_refs[original_ref] += 1
+ else:
+ self.found_refs[original_ref] = 1
+ return reference
+
+ def findFootnotesPlaceholder(self, root):
+ """ Return ElementTree Element that contains Footnote placeholder. """
+ def finder(element):
+ for child in element:
+ if child.text:
+ if child.text.find(self.getConfig("PLACE_MARKER")) > -1:
+ return child, element, True
+ if child.tail:
+ if child.tail.find(self.getConfig("PLACE_MARKER")) > -1:
+ return child, element, False
+ child_res = finder(child)
+ if child_res is not None:
+ return child_res
+ return None
+
+ res = finder(root)
+ return res
+
+ def setFootnote(self, id, text):
+ """ Store a footnote for later retrieval. """
+ self.footnotes[id] = text
+
+ def get_separator(self):
+ """ Get the footnote separator. """
+ return self.getConfig("SEPARATOR")
+
+ def makeFootnoteId(self, id):
+ """ Return footnote link id. """
+ if self.getConfig("UNIQUE_IDS"):
+ return 'fn%s%d-%s' % (self.get_separator(), self.unique_prefix, id)
+ else:
+ return 'fn{}{}'.format(self.get_separator(), id)
+
+ def makeFootnoteRefId(self, id, found=False):
+ """ Return footnote back-link id. """
+ if self.getConfig("UNIQUE_IDS"):
+ return self.unique_ref('fnref%s%d-%s' % (self.get_separator(), self.unique_prefix, id), found)
+ else:
+ return self.unique_ref('fnref{}{}'.format(self.get_separator(), id), found)
+
+ def makeFootnotesDiv(self, root):
+ """ Return div of footnotes as et Element. """
+
+ if not list(self.footnotes.keys()):
+ return None
+
+ div = etree.Element("div")
+ div.set('class', 'footnote')
+ etree.SubElement(div, "hr")
+ ol = etree.SubElement(div, "ol")
+ surrogate_parent = etree.Element("div")
+
+ for index, id in enumerate(self.footnotes.keys(), start=1):
+ li = etree.SubElement(ol, "li")
+ li.set("id", self.makeFootnoteId(id))
+ # Parse footnote with surrogate parent as li cannot be used.
+ # List block handlers have special logic to deal with li.
+ # When we are done parsing, we will copy everything over to li.
+ self.parser.parseChunk(surrogate_parent, self.footnotes[id])
+ for el in list(surrogate_parent):
+ li.append(el)
+ surrogate_parent.remove(el)
+ backlink = etree.Element("a")
+ backlink.set("href", "#" + self.makeFootnoteRefId(id))
+ backlink.set("class", "footnote-backref")
+ backlink.set(
+ "title",
+ self.getConfig("BACKLINK_TITLE") % (index)
+ )
+ backlink.text = FN_BACKLINK_TEXT
+
+ if len(li):
+ node = li[-1]
+ if node.tag == "p":
+ node.text = node.text + NBSP_PLACEHOLDER
+ node.append(backlink)
+ else:
+ p = etree.SubElement(li, "p")
+ p.append(backlink)
+ return div
+
+
+class FootnoteBlockProcessor(BlockProcessor):
+ """ Find all footnote references and store for later use. """
+
+ RE = re.compile(r'^[ ]{0,3}\[\^([^\]]*)\]:[ ]*(.*)$', re.MULTILINE)
+
+ def __init__(self, footnotes):
+ super().__init__(footnotes.parser)
+ self.footnotes = footnotes
+
+ def test(self, parent, block):
+ return True
+
+ def run(self, parent, blocks):
+ """ Find, set, and remove footnote definitions. """
+ block = blocks.pop(0)
+ m = self.RE.search(block)
+ if m:
+ id = m.group(1)
+ fn_blocks = [m.group(2)]
+
+ # Handle rest of block
+ therest = block[m.end():].lstrip('\n')
+ m2 = self.RE.search(therest)
+ if m2:
+ # Another footnote exists in the rest of this block.
+ # Any content before match is continuation of this footnote, which may be lazily indented.
+ before = therest[:m2.start()].rstrip('\n')
+ fn_blocks[0] = '\n'.join([fn_blocks[0], self.detab(before)]).lstrip('\n')
+ # Add back to blocks everything from begining of match forward for next iteration.
+ blocks.insert(0, therest[m2.start():])
+ else:
+ # All remaining lines of block are continuation of this footnote, which may be lazily indented.
+ fn_blocks[0] = '\n'.join([fn_blocks[0], self.detab(therest)]).strip('\n')
+
+ # Check for child elements in remaining blocks.
+ fn_blocks.extend(self.detectTabbed(blocks))
+
+ footnote = "\n\n".join(fn_blocks)
+ self.footnotes.setFootnote(id, footnote.rstrip())
+
+ if block[:m.start()].strip():
+ # Add any content before match back to blocks as separate block
+ blocks.insert(0, block[:m.start()].rstrip('\n'))
+ return True
+ # No match. Restore block.
+ blocks.insert(0, block)
+ return False
+
+ def detectTabbed(self, blocks):
+ """ Find indented text and remove indent before further proccesing.
+
+ Returns: a list of blocks with indentation removed.
+ """
+ fn_blocks = []
+ while blocks:
+ if blocks[0].startswith(' '*4):
+ block = blocks.pop(0)
+ # Check for new footnotes within this block and split at new footnote.
+ m = self.RE.search(block)
+ if m:
+ # Another footnote exists in this block.
+ # Any content before match is continuation of this footnote, which may be lazily indented.
+ before = block[:m.start()].rstrip('\n')
+ fn_blocks.append(self.detab(before))
+ # Add back to blocks everything from begining of match forward for next iteration.
+ blocks.insert(0, block[m.start():])
+ # End of this footnote.
+ break
+ else:
+ # Entire block is part of this footnote.
+ fn_blocks.append(self.detab(block))
+ else:
+ # End of this footnote.
+ break
+ return fn_blocks
+
+ def detab(self, block):
+ """ Remove one level of indent from a block.
+
+ Preserve lazily indented blocks by only removing indent from indented lines.
+ """
+ lines = block.split('\n')
+ for i, line in enumerate(lines):
+ if line.startswith(' '*4):
+ lines[i] = line[4:]
+ return '\n'.join(lines)
+
+
+class FootnoteInlineProcessor(InlineProcessor):
+ """ InlinePattern for footnote markers in a document's body text. """
+
+ def __init__(self, pattern, footnotes):
+ super().__init__(pattern)
+ self.footnotes = footnotes
+
+ def handleMatch(self, m, data):
+ id = m.group(1)
+ if id in self.footnotes.footnotes.keys():
+ sup = etree.Element("sup")
+ a = etree.SubElement(sup, "a")
+ sup.set('id', self.footnotes.makeFootnoteRefId(id, found=True))
+ a.set('href', '#' + self.footnotes.makeFootnoteId(id))
+ a.set('class', 'footnote-ref')
+ a.text = str(list(self.footnotes.footnotes.keys()).index(id) + 1)
+ return sup, m.start(0), m.end(0)
+ else:
+ return None, None, None
+
+
+class FootnotePostTreeprocessor(Treeprocessor):
+ """ Amend footnote div with duplicates. """
+
+ def __init__(self, footnotes):
+ self.footnotes = footnotes
+
+ def add_duplicates(self, li, duplicates):
+ """ Adjust current li and add the duplicates: fnref2, fnref3, etc. """
+ for link in li.iter('a'):
+ # Find the link that needs to be duplicated.
+ if link.attrib.get('class', '') == 'footnote-backref':
+ ref, rest = link.attrib['href'].split(self.footnotes.get_separator(), 1)
+ # Duplicate link the number of times we need to
+ # and point the to the appropriate references.
+ links = []
+ for index in range(2, duplicates + 1):
+ sib_link = copy.deepcopy(link)
+ sib_link.attrib['href'] = '%s%d%s%s' % (ref, index, self.footnotes.get_separator(), rest)
+ links.append(sib_link)
+ self.offset += 1
+ # Add all the new duplicate links.
+ el = list(li)[-1]
+ for link in links:
+ el.append(link)
+ break
+
+ def get_num_duplicates(self, li):
+ """ Get the number of duplicate refs of the footnote. """
+ fn, rest = li.attrib.get('id', '').split(self.footnotes.get_separator(), 1)
+ link_id = '{}ref{}{}'.format(fn, self.footnotes.get_separator(), rest)
+ return self.footnotes.found_refs.get(link_id, 0)
+
+ def handle_duplicates(self, parent):
+ """ Find duplicate footnotes and format and add the duplicates. """
+ for li in list(parent):
+ # Check number of duplicates footnotes and insert
+ # additional links if needed.
+ count = self.get_num_duplicates(li)
+ if count > 1:
+ self.add_duplicates(li, count)
+
+ def run(self, root):
+ """ Crawl the footnote div and add missing duplicate footnotes. """
+ self.offset = 0
+ for div in root.iter('div'):
+ if div.attrib.get('class', '') == 'footnote':
+ # Footnotes shoul be under the first orderd list under
+ # the footnote div. So once we find it, quit.
+ for ol in div.iter('ol'):
+ self.handle_duplicates(ol)
+ break
+
+
+class FootnoteTreeprocessor(Treeprocessor):
+ """ Build and append footnote div to end of document. """
+
+ def __init__(self, footnotes):
+ self.footnotes = footnotes
+
+ def run(self, root):
+ footnotesDiv = self.footnotes.makeFootnotesDiv(root)
+ if footnotesDiv is not None:
+ result = self.footnotes.findFootnotesPlaceholder(root)
+ if result:
+ child, parent, isText = result
+ ind = list(parent).index(child)
+ if isText:
+ parent.remove(child)
+ parent.insert(ind, footnotesDiv)
+ else:
+ parent.insert(ind + 1, footnotesDiv)
+ child.tail = None
+ else:
+ root.append(footnotesDiv)
+
+
+class FootnotePostprocessor(Postprocessor):
+ """ Replace placeholders with html entities. """
+ def __init__(self, footnotes):
+ self.footnotes = footnotes
+
+ def run(self, text):
+ text = text.replace(
+ FN_BACKLINK_TEXT, self.footnotes.getConfig("BACKLINK_TEXT")
+ )
+ return text.replace(NBSP_PLACEHOLDER, " ")
+
+
+def makeExtension(**kwargs): # pragma: no cover
+ """ Return an instance of the FootnoteExtension """
+ return FootnoteExtension(**kwargs)
diff --git a/venv/Lib/site-packages/markdown/extensions/legacy_attrs.py b/venv/Lib/site-packages/markdown/extensions/legacy_attrs.py
new file mode 100644
index 0000000..b51d778
--- /dev/null
+++ b/venv/Lib/site-packages/markdown/extensions/legacy_attrs.py
@@ -0,0 +1,67 @@
+"""
+Python Markdown
+
+A Python implementation of John Gruber's Markdown.
+
+Documentation: https://python-markdown.github.io/
+GitHub: https://github.com/Python-Markdown/markdown/
+PyPI: https://pypi.org/project/Markdown/
+
+Started by Manfred Stienstra (http://www.dwerg.net/).
+Maintained for a few years by Yuri Takhteyev (http://www.freewisdom.org).
+Currently maintained by Waylan Limberg (https://github.com/waylan),
+Dmitry Shachnev (https://github.com/mitya57) and Isaac Muse (https://github.com/facelessuser).
+
+Copyright 2007-2018 The Python Markdown Project (v. 1.7 and later)
+Copyright 2004, 2005, 2006 Yuri Takhteyev (v. 0.2-1.6b)
+Copyright 2004 Manfred Stienstra (the original version)
+
+License: BSD (see LICENSE.md for details).
+
+Legacy Attributes Extension
+===========================
+
+An extension to Python Markdown which implements legacy attributes.
+
+Prior to Python-Markdown version 3.0, the Markdown class had an `enable_attributes`
+keyword which was on by default and provided for attributes to be defined for elements
+using the format `{@key=value}`. This extension is provided as a replacement for
+backward compatability. New documents should be authored using attr_lists. However,
+numerious documents exist which have been using the old attribute format for many
+years. This extension can be used to continue to render those documents correctly.
+"""
+
+import re
+from markdown.treeprocessors import Treeprocessor, isString
+from markdown.extensions import Extension
+
+
+ATTR_RE = re.compile(r'\{@([^\}]*)=([^\}]*)}') # {@id=123}
+
+
+class LegacyAttrs(Treeprocessor):
+ def run(self, doc):
+ """Find and set values of attributes ({@key=value}). """
+ for el in doc.iter():
+ alt = el.get('alt', None)
+ if alt is not None:
+ el.set('alt', self.handleAttributes(el, alt))
+ if el.text and isString(el.text):
+ el.text = self.handleAttributes(el, el.text)
+ if el.tail and isString(el.tail):
+ el.tail = self.handleAttributes(el, el.tail)
+
+ def handleAttributes(self, el, txt):
+ """ Set attributes and return text without definitions. """
+ def attributeCallback(match):
+ el.set(match.group(1), match.group(2).replace('\n', ' '))
+ return ATTR_RE.sub(attributeCallback, txt)
+
+
+class LegacyAttrExtension(Extension):
+ def extendMarkdown(self, md):
+ md.treeprocessors.register(LegacyAttrs(md), 'legacyattrs', 15)
+
+
+def makeExtension(**kwargs): # pragma: no cover
+ return LegacyAttrExtension(**kwargs)
diff --git a/venv/Lib/site-packages/markdown/extensions/legacy_em.py b/venv/Lib/site-packages/markdown/extensions/legacy_em.py
new file mode 100644
index 0000000..7fddb77
--- /dev/null
+++ b/venv/Lib/site-packages/markdown/extensions/legacy_em.py
@@ -0,0 +1,49 @@
+'''
+Legacy Em Extension for Python-Markdown
+=======================================
+
+This extention provides legacy behavior for _connected_words_.
+
+Copyright 2015-2018 The Python Markdown Project
+
+License: [BSD](https://opensource.org/licenses/bsd-license.php)
+
+'''
+
+from . import Extension
+from ..inlinepatterns import UnderscoreProcessor, EmStrongItem, EM_STRONG2_RE, STRONG_EM2_RE
+import re
+
+# _emphasis_
+EMPHASIS_RE = r'(_)([^_]+)\1'
+
+# __strong__
+STRONG_RE = r'(_{2})(.+?)\1'
+
+# __strong_em___
+STRONG_EM_RE = r'(_)\1(?!\1)([^_]+?)\1(?!\1)(.+?)\1{3}'
+
+
+class LegacyUnderscoreProcessor(UnderscoreProcessor):
+ """Emphasis processor for handling strong and em matches inside underscores."""
+
+ PATTERNS = [
+ EmStrongItem(re.compile(EM_STRONG2_RE, re.DOTALL | re.UNICODE), 'double', 'strong,em'),
+ EmStrongItem(re.compile(STRONG_EM2_RE, re.DOTALL | re.UNICODE), 'double', 'em,strong'),
+ EmStrongItem(re.compile(STRONG_EM_RE, re.DOTALL | re.UNICODE), 'double2', 'strong,em'),
+ EmStrongItem(re.compile(STRONG_RE, re.DOTALL | re.UNICODE), 'single', 'strong'),
+ EmStrongItem(re.compile(EMPHASIS_RE, re.DOTALL | re.UNICODE), 'single', 'em')
+ ]
+
+
+class LegacyEmExtension(Extension):
+ """ Add legacy_em extension to Markdown class."""
+
+ def extendMarkdown(self, md):
+ """ Modify inline patterns. """
+ md.inlinePatterns.register(LegacyUnderscoreProcessor(r'_'), 'em_strong2', 50)
+
+
+def makeExtension(**kwargs): # pragma: no cover
+ """ Return an instance of the LegacyEmExtension """
+ return LegacyEmExtension(**kwargs)
diff --git a/venv/Lib/site-packages/markdown/extensions/md_in_html.py b/venv/Lib/site-packages/markdown/extensions/md_in_html.py
new file mode 100644
index 0000000..81cc15c
--- /dev/null
+++ b/venv/Lib/site-packages/markdown/extensions/md_in_html.py
@@ -0,0 +1,364 @@
+"""
+Python-Markdown Markdown in HTML Extension
+===============================
+
+An implementation of [PHP Markdown Extra](http://michelf.com/projects/php-markdown/extra/)'s
+parsing of Markdown syntax in raw HTML.
+
+See
+for documentation.
+
+Copyright The Python Markdown Project
+
+License: [BSD](https://opensource.org/licenses/bsd-license.php)
+
+"""
+
+from . import Extension
+from ..blockprocessors import BlockProcessor
+from ..preprocessors import Preprocessor
+from ..postprocessors import RawHtmlPostprocessor
+from .. import util
+from ..htmlparser import HTMLExtractor, blank_line_re
+import xml.etree.ElementTree as etree
+
+
+class HTMLExtractorExtra(HTMLExtractor):
+ """
+ Override HTMLExtractor and create etree Elements for any elements which should have content parsed as Markdown.
+ """
+
+ def __init__(self, md, *args, **kwargs):
+ # All block-level tags.
+ self.block_level_tags = set(md.block_level_elements.copy())
+ # Block-level tags in which the content only gets span level parsing
+ self.span_tags = set(
+ ['address', 'dd', 'dt', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'legend', 'li', 'p', 'summary', 'td', 'th']
+ )
+ # Block-level tags which never get their content parsed.
+ self.raw_tags = set(['canvas', 'math', 'option', 'pre', 'script', 'style', 'textarea'])
+
+ super().__init__(md, *args, **kwargs)
+
+ # Block-level tags in which the content gets parsed as blocks
+ self.block_tags = set(self.block_level_tags) - (self.span_tags | self.raw_tags | self.empty_tags)
+ self.span_and_blocks_tags = self.block_tags | self.span_tags
+
+ def reset(self):
+ """Reset this instance. Loses all unprocessed data."""
+ self.mdstack = [] # When markdown=1, stack contains a list of tags
+ self.treebuilder = etree.TreeBuilder()
+ self.mdstate = [] # one of 'block', 'span', 'off', or None
+ super().reset()
+
+ def close(self):
+ """Handle any buffered data."""
+ super().close()
+ # Handle any unclosed tags.
+ if self.mdstack:
+ # Close the outermost parent. handle_endtag will close all unclosed children.
+ self.handle_endtag(self.mdstack[0])
+
+ def get_element(self):
+ """ Return element from treebuilder and reset treebuilder for later use. """
+ element = self.treebuilder.close()
+ self.treebuilder = etree.TreeBuilder()
+ return element
+
+ def get_state(self, tag, attrs):
+ """ Return state from tag and `markdown` attr. One of 'block', 'span', or 'off'. """
+ md_attr = attrs.get('markdown', '0')
+ if md_attr == 'markdown':
+ # `` is the same as ``.
+ md_attr = '1'
+ parent_state = self.mdstate[-1] if self.mdstate else None
+ if parent_state == 'off' or (parent_state == 'span' and md_attr != '0'):
+ # Only use the parent state if it is more restrictive than the markdown attribute.
+ md_attr = parent_state
+ if ((md_attr == '1' and tag in self.block_tags) or
+ (md_attr == 'block' and tag in self.span_and_blocks_tags)):
+ return 'block'
+ elif ((md_attr == '1' and tag in self.span_tags) or
+ (md_attr == 'span' and tag in self.span_and_blocks_tags)):
+ return 'span'
+ elif tag in self.block_level_tags:
+ return 'off'
+ else: # pragma: no cover
+ return None
+
+ def handle_starttag(self, tag, attrs):
+ # Handle tags that should always be empty and do not specify a closing tag
+ if tag in self.empty_tags and (self.at_line_start() or self.intail):
+ attrs = {key: value if value is not None else key for key, value in attrs}
+ if "markdown" in attrs:
+ attrs.pop('markdown')
+ element = etree.Element(tag, attrs)
+ data = etree.tostring(element, encoding='unicode', method='html')
+ else:
+ data = self.get_starttag_text()
+ self.handle_empty_tag(data, True)
+ return
+
+ if tag in self.block_level_tags and (self.at_line_start() or self.intail):
+ # Valueless attr (ex: ``) results in `[('checked', None)]`.
+ # Convert to `{'checked': 'checked'}`.
+ attrs = {key: value if value is not None else key for key, value in attrs}
+ state = self.get_state(tag, attrs)
+ if self.inraw or (state in [None, 'off'] and not self.mdstack):
+ # fall back to default behavior
+ attrs.pop('markdown', None)
+ super().handle_starttag(tag, attrs)
+ else:
+ if 'p' in self.mdstack and tag in self.block_level_tags:
+ # Close unclosed 'p' tag
+ self.handle_endtag('p')
+ self.mdstate.append(state)
+ self.mdstack.append(tag)
+ attrs['markdown'] = state
+ self.treebuilder.start(tag, attrs)
+ else:
+ # Span level tag
+ if self.inraw:
+ super().handle_starttag(tag, attrs)
+ else:
+ text = self.get_starttag_text()
+ if self.mdstate and self.mdstate[-1] == "off":
+ self.handle_data(self.md.htmlStash.store(text))
+ else:
+ self.handle_data(text)
+ if tag in self.CDATA_CONTENT_ELEMENTS:
+ # This is presumably a standalone tag in a code span (see #1036).
+ self.clear_cdata_mode()
+
+ def handle_endtag(self, tag):
+ if tag in self.block_level_tags:
+ if self.inraw:
+ super().handle_endtag(tag)
+ elif tag in self.mdstack:
+ # Close element and any unclosed children
+ while self.mdstack:
+ item = self.mdstack.pop()
+ self.mdstate.pop()
+ self.treebuilder.end(item)
+ if item == tag:
+ break
+ if not self.mdstack:
+ # Last item in stack is closed. Stash it
+ element = self.get_element()
+ # Get last entry to see if it ends in newlines
+ # If it is an element, assume there is no newlines
+ item = self.cleandoc[-1] if self.cleandoc else ''
+ # If we only have one newline before block element, add another
+ if not item.endswith('\n\n') and item.endswith('\n'):
+ self.cleandoc.append('\n')
+ self.cleandoc.append(self.md.htmlStash.store(element))
+ self.cleandoc.append('\n\n')
+ self.state = []
+ # Check if element has a tail
+ if not blank_line_re.match(
+ self.rawdata[self.line_offset + self.offset + len(self.get_endtag_text(tag)):]):
+ # More content exists after endtag.
+ self.intail = True
+ else:
+ # Treat orphan closing tag as a span level tag.
+ text = self.get_endtag_text(tag)
+ if self.mdstate and self.mdstate[-1] == "off":
+ self.handle_data(self.md.htmlStash.store(text))
+ else:
+ self.handle_data(text)
+ else:
+ # Span level tag
+ if self.inraw:
+ super().handle_endtag(tag)
+ else:
+ text = self.get_endtag_text(tag)
+ if self.mdstate and self.mdstate[-1] == "off":
+ self.handle_data(self.md.htmlStash.store(text))
+ else:
+ self.handle_data(text)
+
+ def handle_startendtag(self, tag, attrs):
+ if tag in self.empty_tags:
+ attrs = {key: value if value is not None else key for key, value in attrs}
+ if "markdown" in attrs:
+ attrs.pop('markdown')
+ element = etree.Element(tag, attrs)
+ data = etree.tostring(element, encoding='unicode', method='html')
+ else:
+ data = self.get_starttag_text()
+ else:
+ data = self.get_starttag_text()
+ self.handle_empty_tag(data, is_block=self.md.is_block_level(tag))
+
+ def handle_data(self, data):
+ if self.intail and '\n' in data:
+ self.intail = False
+ if self.inraw or not self.mdstack:
+ super().handle_data(data)
+ else:
+ self.treebuilder.data(data)
+
+ def handle_empty_tag(self, data, is_block):
+ if self.inraw or not self.mdstack:
+ super().handle_empty_tag(data, is_block)
+ else:
+ if self.at_line_start() and is_block:
+ self.handle_data('\n' + self.md.htmlStash.store(data) + '\n\n')
+ else:
+ self.handle_data(self.md.htmlStash.store(data))
+
+ def parse_pi(self, i):
+ if self.at_line_start() or self.intail or self.mdstack:
+ # The same override exists in HTMLExtractor without the check
+ # for mdstack. Therefore, use HTMLExtractor's parent instead.
+ return super(HTMLExtractor, self).parse_pi(i)
+ # This is not the beginning of a raw block so treat as plain data
+ # and avoid consuming any tags which may follow (see #1066).
+ self.handle_data('')
+ return i + 2
+
+ def parse_html_declaration(self, i):
+ if self.at_line_start() or self.intail or self.mdstack:
+ # The same override exists in HTMLExtractor without the check
+ # for mdstack. Therefore, use HTMLExtractor's parent instead.
+ return super(HTMLExtractor, self).parse_html_declaration(i)
+ # This is not the beginning of a raw block so treat as plain data
+ # and avoid consuming any tags which may follow (see #1066).
+ self.handle_data('
+for documentation.
+
+Original code Copyright 2007-2008 [Waylan Limberg](http://achinghead.com).
+
+All changes Copyright 2008-2014 The Python Markdown Project
+
+License: [BSD](https://opensource.org/licenses/bsd-license.php)
+
+"""
+
+from . import Extension
+from ..preprocessors import Preprocessor
+import re
+import logging
+
+log = logging.getLogger('MARKDOWN')
+
+# Global Vars
+META_RE = re.compile(r'^[ ]{0,3}(?P[A-Za-z0-9_-]+):\s*(?P.*)')
+META_MORE_RE = re.compile(r'^[ ]{4,}(?P.*)')
+BEGIN_RE = re.compile(r'^-{3}(\s.*)?')
+END_RE = re.compile(r'^(-{3}|\.{3})(\s.*)?')
+
+
+class MetaExtension (Extension):
+ """ Meta-Data extension for Python-Markdown. """
+
+ def extendMarkdown(self, md):
+ """ Add MetaPreprocessor to Markdown instance. """
+ md.registerExtension(self)
+ self.md = md
+ md.preprocessors.register(MetaPreprocessor(md), 'meta', 27)
+
+ def reset(self):
+ self.md.Meta = {}
+
+
+class MetaPreprocessor(Preprocessor):
+ """ Get Meta-Data. """
+
+ def run(self, lines):
+ """ Parse Meta-Data and store in Markdown.Meta. """
+ meta = {}
+ key = None
+ if lines and BEGIN_RE.match(lines[0]):
+ lines.pop(0)
+ while lines:
+ line = lines.pop(0)
+ m1 = META_RE.match(line)
+ if line.strip() == '' or END_RE.match(line):
+ break # blank line or end of YAML header - done
+ if m1:
+ key = m1.group('key').lower().strip()
+ value = m1.group('value').strip()
+ try:
+ meta[key].append(value)
+ except KeyError:
+ meta[key] = [value]
+ else:
+ m2 = META_MORE_RE.match(line)
+ if m2 and key:
+ # Add another line to existing key
+ meta[key].append(m2.group('value').strip())
+ else:
+ lines.insert(0, line)
+ break # no meta data - done
+ self.md.Meta = meta
+ return lines
+
+
+def makeExtension(**kwargs): # pragma: no cover
+ return MetaExtension(**kwargs)
diff --git a/venv/Lib/site-packages/markdown/extensions/nl2br.py b/venv/Lib/site-packages/markdown/extensions/nl2br.py
new file mode 100644
index 0000000..6c7491b
--- /dev/null
+++ b/venv/Lib/site-packages/markdown/extensions/nl2br.py
@@ -0,0 +1,33 @@
+"""
+NL2BR Extension
+===============
+
+A Python-Markdown extension to treat newlines as hard breaks; like
+GitHub-flavored Markdown does.
+
+See
+for documentation.
+
+Oringinal code Copyright 2011 [Brian Neal](https://deathofagremmie.com/)
+
+All changes Copyright 2011-2014 The Python Markdown Project
+
+License: [BSD](https://opensource.org/licenses/bsd-license.php)
+
+"""
+
+from . import Extension
+from ..inlinepatterns import SubstituteTagInlineProcessor
+
+BR_RE = r'\n'
+
+
+class Nl2BrExtension(Extension):
+
+ def extendMarkdown(self, md):
+ br_tag = SubstituteTagInlineProcessor(BR_RE, 'br')
+ md.inlinePatterns.register(br_tag, 'nl', 5)
+
+
+def makeExtension(**kwargs): # pragma: no cover
+ return Nl2BrExtension(**kwargs)
diff --git a/venv/Lib/site-packages/markdown/extensions/sane_lists.py b/venv/Lib/site-packages/markdown/extensions/sane_lists.py
new file mode 100644
index 0000000..e27eb18
--- /dev/null
+++ b/venv/Lib/site-packages/markdown/extensions/sane_lists.py
@@ -0,0 +1,54 @@
+"""
+Sane List Extension for Python-Markdown
+=======================================
+
+Modify the behavior of Lists in Python-Markdown to act in a sane manor.
+
+See
+for documentation.
+
+Original code Copyright 2011 [Waylan Limberg](http://achinghead.com)
+
+All changes Copyright 2011-2014 The Python Markdown Project
+
+License: [BSD](https://opensource.org/licenses/bsd-license.php)
+
+"""
+
+from . import Extension
+from ..blockprocessors import OListProcessor, UListProcessor
+import re
+
+
+class SaneOListProcessor(OListProcessor):
+
+ SIBLING_TAGS = ['ol']
+ LAZY_OL = False
+
+ def __init__(self, parser):
+ super().__init__(parser)
+ self.CHILD_RE = re.compile(r'^[ ]{0,%d}((\d+\.))[ ]+(.*)' %
+ (self.tab_length - 1))
+
+
+class SaneUListProcessor(UListProcessor):
+
+ SIBLING_TAGS = ['ul']
+
+ def __init__(self, parser):
+ super().__init__(parser)
+ self.CHILD_RE = re.compile(r'^[ ]{0,%d}(([*+-]))[ ]+(.*)' %
+ (self.tab_length - 1))
+
+
+class SaneListExtension(Extension):
+ """ Add sane lists to Markdown. """
+
+ def extendMarkdown(self, md):
+ """ Override existing Processors. """
+ md.parser.blockprocessors.register(SaneOListProcessor(md.parser), 'olist', 40)
+ md.parser.blockprocessors.register(SaneUListProcessor(md.parser), 'ulist', 30)
+
+
+def makeExtension(**kwargs): # pragma: no cover
+ return SaneListExtension(**kwargs)
diff --git a/venv/Lib/site-packages/markdown/extensions/smarty.py b/venv/Lib/site-packages/markdown/extensions/smarty.py
new file mode 100644
index 0000000..894805f
--- /dev/null
+++ b/venv/Lib/site-packages/markdown/extensions/smarty.py
@@ -0,0 +1,263 @@
+'''
+Smarty extension for Python-Markdown
+====================================
+
+Adds conversion of ASCII dashes, quotes and ellipses to their HTML
+entity equivalents.
+
+See
+for documentation.
+
+Author: 2013, Dmitry Shachnev
+
+All changes Copyright 2013-2014 The Python Markdown Project
+
+License: [BSD](https://opensource.org/licenses/bsd-license.php)
+
+SmartyPants license:
+
+ Copyright (c) 2003 John Gruber
+ All rights reserved.
+
+ Redistribution and use in source and binary forms, with or without
+ modification, are permitted provided that the following conditions are
+ met:
+
+ * Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+ * Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in
+ the documentation and/or other materials provided with the
+ distribution.
+
+ * Neither the name "SmartyPants" nor the names of its contributors
+ may be used to endorse or promote products derived from this
+ software without specific prior written permission.
+
+ This software is provided by the copyright holders and contributors "as
+ is" and any express or implied warranties, including, but not limited
+ to, the implied warranties of merchantability and fitness for a
+ particular purpose are disclaimed. In no event shall the copyright
+ owner or contributors be liable for any direct, indirect, incidental,
+ special, exemplary, or consequential damages (including, but not
+ limited to, procurement of substitute goods or services; loss of use,
+ data, or profits; or business interruption) however caused and on any
+ theory of liability, whether in contract, strict liability, or tort
+ (including negligence or otherwise) arising in any way out of the use
+ of this software, even if advised of the possibility of such damage.
+
+
+smartypants.py license:
+
+ smartypants.py is a derivative work of SmartyPants.
+ Copyright (c) 2004, 2007 Chad Miller
+
+ Redistribution and use in source and binary forms, with or without
+ modification, are permitted provided that the following conditions are
+ met:
+
+ * Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+ * Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in
+ the documentation and/or other materials provided with the
+ distribution.
+
+ This software is provided by the copyright holders and contributors "as
+ is" and any express or implied warranties, including, but not limited
+ to, the implied warranties of merchantability and fitness for a
+ particular purpose are disclaimed. In no event shall the copyright
+ owner or contributors be liable for any direct, indirect, incidental,
+ special, exemplary, or consequential damages (including, but not
+ limited to, procurement of substitute goods or services; loss of use,
+ data, or profits; or business interruption) however caused and on any
+ theory of liability, whether in contract, strict liability, or tort
+ (including negligence or otherwise) arising in any way out of the use
+ of this software, even if advised of the possibility of such damage.
+
+'''
+
+
+from . import Extension
+from ..inlinepatterns import HtmlInlineProcessor, HTML_RE
+from ..treeprocessors import InlineProcessor
+from ..util import Registry, deprecated
+
+
+# Constants for quote education.
+punctClass = r"""[!"#\$\%'()*+,-.\/:;<=>?\@\[\\\]\^_`{|}~]"""
+endOfWordClass = r"[\s.,;:!?)]"
+closeClass = r"[^\ \t\r\n\[\{\(\-\u0002\u0003]"
+
+openingQuotesBase = (
+ r'(\s' # a whitespace char
+ r'| ' # or a non-breaking space entity
+ r'|--' # or dashes
+ r'|–|—' # or unicode
+ r'|&[mn]dash;' # or named dash entities
+ r'|–|—' # or decimal entities
+ r')'
+)
+
+substitutions = {
+ 'mdash': '—',
+ 'ndash': '–',
+ 'ellipsis': '…',
+ 'left-angle-quote': '«',
+ 'right-angle-quote': '»',
+ 'left-single-quote': '‘',
+ 'right-single-quote': '’',
+ 'left-double-quote': '“',
+ 'right-double-quote': '”',
+}
+
+
+# Special case if the very first character is a quote
+# followed by punctuation at a non-word-break. Close the quotes by brute force:
+singleQuoteStartRe = r"^'(?=%s\B)" % punctClass
+doubleQuoteStartRe = r'^"(?=%s\B)' % punctClass
+
+# Special case for double sets of quotes, e.g.:
+#
He said, "'Quoted' words in a larger quote."
+doubleQuoteSetsRe = r""""'(?=\w)"""
+singleQuoteSetsRe = r"""'"(?=\w)"""
+
+# Special case for decade abbreviations (the '80s):
+decadeAbbrRe = r"(?)'
+
+
+class SubstituteTextPattern(HtmlInlineProcessor):
+ def __init__(self, pattern, replace, md):
+ """ Replaces matches with some text. """
+ HtmlInlineProcessor.__init__(self, pattern)
+ self.replace = replace
+ self.md = md
+
+ @property
+ @deprecated("Use 'md' instead.")
+ def markdown(self):
+ # TODO: remove this later
+ return self.md
+
+ def handleMatch(self, m, data):
+ result = ''
+ for part in self.replace:
+ if isinstance(part, int):
+ result += m.group(part)
+ else:
+ result += self.md.htmlStash.store(part)
+ return result, m.start(0), m.end(0)
+
+
+class SmartyExtension(Extension):
+ def __init__(self, **kwargs):
+ self.config = {
+ 'smart_quotes': [True, 'Educate quotes'],
+ 'smart_angled_quotes': [False, 'Educate angled quotes'],
+ 'smart_dashes': [True, 'Educate dashes'],
+ 'smart_ellipses': [True, 'Educate ellipses'],
+ 'substitutions': [{}, 'Overwrite default substitutions'],
+ }
+ super().__init__(**kwargs)
+ self.substitutions = dict(substitutions)
+ self.substitutions.update(self.getConfig('substitutions', default={}))
+
+ def _addPatterns(self, md, patterns, serie, priority):
+ for ind, pattern in enumerate(patterns):
+ pattern += (md,)
+ pattern = SubstituteTextPattern(*pattern)
+ name = 'smarty-%s-%d' % (serie, ind)
+ self.inlinePatterns.register(pattern, name, priority-ind)
+
+ def educateDashes(self, md):
+ emDashesPattern = SubstituteTextPattern(
+ r'(?\>', (self.substitutions['right-angle-quote'],), md
+ )
+ self.inlinePatterns.register(leftAngledQuotePattern, 'smarty-left-angle-quotes', 40)
+ self.inlinePatterns.register(rightAngledQuotePattern, 'smarty-right-angle-quotes', 35)
+
+ def educateQuotes(self, md):
+ lsquo = self.substitutions['left-single-quote']
+ rsquo = self.substitutions['right-single-quote']
+ ldquo = self.substitutions['left-double-quote']
+ rdquo = self.substitutions['right-double-quote']
+ patterns = (
+ (singleQuoteStartRe, (rsquo,)),
+ (doubleQuoteStartRe, (rdquo,)),
+ (doubleQuoteSetsRe, (ldquo + lsquo,)),
+ (singleQuoteSetsRe, (lsquo + ldquo,)),
+ (decadeAbbrRe, (rsquo,)),
+ (openingSingleQuotesRegex, (1, lsquo)),
+ (closingSingleQuotesRegex, (rsquo,)),
+ (closingSingleQuotesRegex2, (rsquo, 1)),
+ (remainingSingleQuotesRegex, (lsquo,)),
+ (openingDoubleQuotesRegex, (1, ldquo)),
+ (closingDoubleQuotesRegex, (rdquo,)),
+ (closingDoubleQuotesRegex2, (rdquo,)),
+ (remainingDoubleQuotesRegex, (ldquo,))
+ )
+ self._addPatterns(md, patterns, 'quotes', 30)
+
+ def extendMarkdown(self, md):
+ configs = self.getConfigs()
+ self.inlinePatterns = Registry()
+ if configs['smart_ellipses']:
+ self.educateEllipses(md)
+ if configs['smart_quotes']:
+ self.educateQuotes(md)
+ if configs['smart_angled_quotes']:
+ self.educateAngledQuotes(md)
+ # Override HTML_RE from inlinepatterns.py so that it does not
+ # process tags with duplicate closing quotes.
+ md.inlinePatterns.register(HtmlInlineProcessor(HTML_STRICT_RE, md), 'html', 90)
+ if configs['smart_dashes']:
+ self.educateDashes(md)
+ inlineProcessor = InlineProcessor(md)
+ inlineProcessor.inlinePatterns = self.inlinePatterns
+ md.treeprocessors.register(inlineProcessor, 'smarty', 2)
+ md.ESCAPED_CHARS.extend(['"', "'"])
+
+
+def makeExtension(**kwargs): # pragma: no cover
+ return SmartyExtension(**kwargs)
diff --git a/venv/Lib/site-packages/markdown/extensions/tables.py b/venv/Lib/site-packages/markdown/extensions/tables.py
new file mode 100644
index 0000000..4b027bb
--- /dev/null
+++ b/venv/Lib/site-packages/markdown/extensions/tables.py
@@ -0,0 +1,223 @@
+"""
+Tables Extension for Python-Markdown
+====================================
+
+Added parsing of tables to Python-Markdown.
+
+See
+for documentation.
+
+Original code Copyright 2009 [Waylan Limberg](http://achinghead.com)
+
+All changes Copyright 2008-2014 The Python Markdown Project
+
+License: [BSD](https://opensource.org/licenses/bsd-license.php)
+
+"""
+
+from . import Extension
+from ..blockprocessors import BlockProcessor
+import xml.etree.ElementTree as etree
+import re
+PIPE_NONE = 0
+PIPE_LEFT = 1
+PIPE_RIGHT = 2
+
+
+class TableProcessor(BlockProcessor):
+ """ Process Tables. """
+
+ RE_CODE_PIPES = re.compile(r'(?:(\\\\)|(\\`+)|(`+)|(\\\|)|(\|))')
+ RE_END_BORDER = re.compile(r'(? 1:
+ header0 = rows[0]
+ self.border = PIPE_NONE
+ if header0.startswith('|'):
+ self.border |= PIPE_LEFT
+ if self.RE_END_BORDER.search(header0) is not None:
+ self.border |= PIPE_RIGHT
+ row = self._split_row(header0)
+ row0_len = len(row)
+ is_table = row0_len > 1
+
+ # Each row in a single column table needs at least one pipe.
+ if not is_table and row0_len == 1 and self.border:
+ for index in range(1, len(rows)):
+ is_table = rows[index].startswith('|')
+ if not is_table:
+ is_table = self.RE_END_BORDER.search(rows[index]) is not None
+ if not is_table:
+ break
+
+ if is_table:
+ row = self._split_row(rows[1])
+ is_table = (len(row) == row0_len) and set(''.join(row)) <= set('|:- ')
+ if is_table:
+ self.separator = row
+
+ return is_table
+
+ def run(self, parent, blocks):
+ """ Parse a table block and build table. """
+ block = blocks.pop(0).split('\n')
+ header = block[0].strip(' ')
+ rows = [] if len(block) < 3 else block[2:]
+
+ # Get alignment of columns
+ align = []
+ for c in self.separator:
+ c = c.strip(' ')
+ if c.startswith(':') and c.endswith(':'):
+ align.append('center')
+ elif c.startswith(':'):
+ align.append('left')
+ elif c.endswith(':'):
+ align.append('right')
+ else:
+ align.append(None)
+
+ # Build table
+ table = etree.SubElement(parent, 'table')
+ thead = etree.SubElement(table, 'thead')
+ self._build_row(header, thead, align)
+ tbody = etree.SubElement(table, 'tbody')
+ if len(rows) == 0:
+ # Handle empty table
+ self._build_empty_row(tbody, align)
+ else:
+ for row in rows:
+ self._build_row(row.strip(' '), tbody, align)
+
+ def _build_empty_row(self, parent, align):
+ """Build an empty row."""
+ tr = etree.SubElement(parent, 'tr')
+ count = len(align)
+ while count:
+ etree.SubElement(tr, 'td')
+ count -= 1
+
+ def _build_row(self, row, parent, align):
+ """ Given a row of text, build table cells. """
+ tr = etree.SubElement(parent, 'tr')
+ tag = 'td'
+ if parent.tag == 'thead':
+ tag = 'th'
+ cells = self._split_row(row)
+ # We use align here rather than cells to ensure every row
+ # contains the same number of columns.
+ for i, a in enumerate(align):
+ c = etree.SubElement(tr, tag)
+ try:
+ c.text = cells[i].strip(' ')
+ except IndexError: # pragma: no cover
+ c.text = ""
+ if a:
+ c.set('align', a)
+
+ def _split_row(self, row):
+ """ split a row of text into list of cells. """
+ if self.border:
+ if row.startswith('|'):
+ row = row[1:]
+ row = self.RE_END_BORDER.sub('', row)
+ return self._split(row)
+
+ def _split(self, row):
+ """ split a row of text with some code into a list of cells. """
+ elements = []
+ pipes = []
+ tics = []
+ tic_points = []
+ tic_region = []
+ good_pipes = []
+
+ # Parse row
+ # Throw out \\, and \|
+ for m in self.RE_CODE_PIPES.finditer(row):
+ # Store ` data (len, start_pos, end_pos)
+ if m.group(2):
+ # \`+
+ # Store length of each tic group: subtract \
+ tics.append(len(m.group(2)) - 1)
+ # Store start of group, end of group, and escape length
+ tic_points.append((m.start(2), m.end(2) - 1, 1))
+ elif m.group(3):
+ # `+
+ # Store length of each tic group
+ tics.append(len(m.group(3)))
+ # Store start of group, end of group, and escape length
+ tic_points.append((m.start(3), m.end(3) - 1, 0))
+ # Store pipe location
+ elif m.group(5):
+ pipes.append(m.start(5))
+
+ # Pair up tics according to size if possible
+ # Subtract the escape length *only* from the opening.
+ # Walk through tic list and see if tic has a close.
+ # Store the tic region (start of region, end of region).
+ pos = 0
+ tic_len = len(tics)
+ while pos < tic_len:
+ try:
+ tic_size = tics[pos] - tic_points[pos][2]
+ if tic_size == 0:
+ raise ValueError
+ index = tics[pos + 1:].index(tic_size) + 1
+ tic_region.append((tic_points[pos][0], tic_points[pos + index][1]))
+ pos += index + 1
+ except ValueError:
+ pos += 1
+
+ # Resolve pipes. Check if they are within a tic pair region.
+ # Walk through pipes comparing them to each region.
+ # - If pipe position is less that a region, it isn't in a region
+ # - If it is within a region, we don't want it, so throw it out
+ # - If we didn't throw it out, it must be a table pipe
+ for pipe in pipes:
+ throw_out = False
+ for region in tic_region:
+ if pipe < region[0]:
+ # Pipe is not in a region
+ break
+ elif region[0] <= pipe <= region[1]:
+ # Pipe is within a code region. Throw it out.
+ throw_out = True
+ break
+ if not throw_out:
+ good_pipes.append(pipe)
+
+ # Split row according to table delimeters.
+ pos = 0
+ for pipe in good_pipes:
+ elements.append(row[pos:pipe])
+ pos = pipe + 1
+ elements.append(row[pos:])
+ return elements
+
+
+class TableExtension(Extension):
+ """ Add tables to Markdown. """
+
+ def extendMarkdown(self, md):
+ """ Add an instance of TableProcessor to BlockParser. """
+ if '|' not in md.ESCAPED_CHARS:
+ md.ESCAPED_CHARS.append('|')
+ md.parser.blockprocessors.register(TableProcessor(md.parser), 'table', 75)
+
+
+def makeExtension(**kwargs): # pragma: no cover
+ return TableExtension(**kwargs)
diff --git a/venv/Lib/site-packages/markdown/extensions/toc.py b/venv/Lib/site-packages/markdown/extensions/toc.py
new file mode 100644
index 0000000..e4dc378
--- /dev/null
+++ b/venv/Lib/site-packages/markdown/extensions/toc.py
@@ -0,0 +1,380 @@
+"""
+Table of Contents Extension for Python-Markdown
+===============================================
+
+See
+for documentation.
+
+Oringinal code Copyright 2008 [Jack Miller](https://codezen.org/)
+
+All changes Copyright 2008-2014 The Python Markdown Project
+
+License: [BSD](https://opensource.org/licenses/bsd-license.php)
+
+"""
+
+from . import Extension
+from ..treeprocessors import Treeprocessor
+from ..util import code_escape, parseBoolValue, AMP_SUBSTITUTE, HTML_PLACEHOLDER_RE, AtomicString
+from ..postprocessors import UnescapePostprocessor
+import re
+import html
+import unicodedata
+import xml.etree.ElementTree as etree
+
+
+def slugify(value, separator, unicode=False):
+ """ Slugify a string, to make it URL friendly. """
+ if not unicode:
+ # Replace Extended Latin characters with ASCII, i.e. žlutý → zluty
+ value = unicodedata.normalize('NFKD', value)
+ value = value.encode('ascii', 'ignore').decode('ascii')
+ value = re.sub(r'[^\w\s-]', '', value).strip().lower()
+ return re.sub(r'[{}\s]+'.format(separator), separator, value)
+
+
+def slugify_unicode(value, separator):
+ """ Slugify a string, to make it URL friendly while preserving Unicode characters. """
+ return slugify(value, separator, unicode=True)
+
+
+IDCOUNT_RE = re.compile(r'^(.*)_([0-9]+)$')
+
+
+def unique(id, ids):
+ """ Ensure id is unique in set of ids. Append '_1', '_2'... if not """
+ while id in ids or not id:
+ m = IDCOUNT_RE.match(id)
+ if m:
+ id = '%s_%d' % (m.group(1), int(m.group(2))+1)
+ else:
+ id = '%s_%d' % (id, 1)
+ ids.add(id)
+ return id
+
+
+def get_name(el):
+ """Get title name."""
+
+ text = []
+ for c in el.itertext():
+ if isinstance(c, AtomicString):
+ text.append(html.unescape(c))
+ else:
+ text.append(c)
+ return ''.join(text).strip()
+
+
+def stashedHTML2text(text, md, strip_entities=True):
+ """ Extract raw HTML from stash, reduce to plain text and swap with placeholder. """
+ def _html_sub(m):
+ """ Substitute raw html with plain text. """
+ try:
+ raw = md.htmlStash.rawHtmlBlocks[int(m.group(1))]
+ except (IndexError, TypeError): # pragma: no cover
+ return m.group(0)
+ # Strip out tags and/or entities - leaving text
+ res = re.sub(r'(<[^>]+>)', '', raw)
+ if strip_entities:
+ res = re.sub(r'(&[\#a-zA-Z0-9]+;)', '', res)
+ return res
+
+ return HTML_PLACEHOLDER_RE.sub(_html_sub, text)
+
+
+def unescape(text):
+ """ Unescape escaped text. """
+ c = UnescapePostprocessor()
+ return c.run(text)
+
+
+def nest_toc_tokens(toc_list):
+ """Given an unsorted list with errors and skips, return a nested one.
+ [{'level': 1}, {'level': 2}]
+ =>
+ [{'level': 1, 'children': [{'level': 2, 'children': []}]}]
+
+ A wrong list is also converted:
+ [{'level': 2}, {'level': 1}]
+ =>
+ [{'level': 2, 'children': []}, {'level': 1, 'children': []}]
+ """
+
+ ordered_list = []
+ if len(toc_list):
+ # Initialize everything by processing the first entry
+ last = toc_list.pop(0)
+ last['children'] = []
+ levels = [last['level']]
+ ordered_list.append(last)
+ parents = []
+
+ # Walk the rest nesting the entries properly
+ while toc_list:
+ t = toc_list.pop(0)
+ current_level = t['level']
+ t['children'] = []
+
+ # Reduce depth if current level < last item's level
+ if current_level < levels[-1]:
+ # Pop last level since we know we are less than it
+ levels.pop()
+
+ # Pop parents and levels we are less than or equal to
+ to_pop = 0
+ for p in reversed(parents):
+ if current_level <= p['level']:
+ to_pop += 1
+ else: # pragma: no cover
+ break
+ if to_pop:
+ levels = levels[:-to_pop]
+ parents = parents[:-to_pop]
+
+ # Note current level as last
+ levels.append(current_level)
+
+ # Level is the same, so append to
+ # the current parent (if available)
+ if current_level == levels[-1]:
+ (parents[-1]['children'] if parents
+ else ordered_list).append(t)
+
+ # Current level is > last item's level,
+ # So make last item a parent and append current as child
+ else:
+ last['children'].append(t)
+ parents.append(last)
+ levels.append(current_level)
+ last = t
+
+ return ordered_list
+
+
+class TocTreeprocessor(Treeprocessor):
+ def __init__(self, md, config):
+ super().__init__(md)
+
+ self.marker = config["marker"]
+ self.title = config["title"]
+ self.base_level = int(config["baselevel"]) - 1
+ self.slugify = config["slugify"]
+ self.sep = config["separator"]
+ self.use_anchors = parseBoolValue(config["anchorlink"])
+ self.anchorlink_class = config["anchorlink_class"]
+ self.use_permalinks = parseBoolValue(config["permalink"], False)
+ if self.use_permalinks is None:
+ self.use_permalinks = config["permalink"]
+ self.permalink_class = config["permalink_class"]
+ self.permalink_title = config["permalink_title"]
+ self.header_rgx = re.compile("[Hh][123456]")
+ if isinstance(config["toc_depth"], str) and '-' in config["toc_depth"]:
+ self.toc_top, self.toc_bottom = [int(x) for x in config["toc_depth"].split('-')]
+ else:
+ self.toc_top = 1
+ self.toc_bottom = int(config["toc_depth"])
+
+ def iterparent(self, node):
+ ''' Iterator wrapper to get allowed parent and child all at once. '''
+
+ # We do not allow the marker inside a header as that
+ # would causes an enless loop of placing a new TOC
+ # inside previously generated TOC.
+ for child in node:
+ if not self.header_rgx.match(child.tag) and child.tag not in ['pre', 'code']:
+ yield node, child
+ yield from self.iterparent(child)
+
+ def replace_marker(self, root, elem):
+ ''' Replace marker with elem. '''
+ for (p, c) in self.iterparent(root):
+ text = ''.join(c.itertext()).strip()
+ if not text:
+ continue
+
+ # To keep the output from screwing up the
+ # validation by putting a
inside of a
+ # we actually replace the
in its entirety.
+
+ # The
element may contain more than a single text content
+ # (nl2br can introduce a ). In this situation, c.text returns
+ # the very first content, ignore children contents or tail content.
+ # len(c) == 0 is here to ensure there is only text in the
.
+ if c.text and c.text.strip() == self.marker and len(c) == 0:
+ for i in range(len(p)):
+ if p[i] == c:
+ p[i] = elem
+ break
+
+ def set_level(self, elem):
+ ''' Adjust header level according to base level. '''
+ level = int(elem.tag[-1]) + self.base_level
+ if level > 6:
+ level = 6
+ elem.tag = 'h%d' % level
+
+ def add_anchor(self, c, elem_id): # @ReservedAssignment
+ anchor = etree.Element("a")
+ anchor.text = c.text
+ anchor.attrib["href"] = "#" + elem_id
+ anchor.attrib["class"] = self.anchorlink_class
+ c.text = ""
+ for elem in c:
+ anchor.append(elem)
+ while len(c):
+ c.remove(c[0])
+ c.append(anchor)
+
+ def add_permalink(self, c, elem_id):
+ permalink = etree.Element("a")
+ permalink.text = ("%spara;" % AMP_SUBSTITUTE
+ if self.use_permalinks is True
+ else self.use_permalinks)
+ permalink.attrib["href"] = "#" + elem_id
+ permalink.attrib["class"] = self.permalink_class
+ if self.permalink_title:
+ permalink.attrib["title"] = self.permalink_title
+ c.append(permalink)
+
+ def build_toc_div(self, toc_list):
+ """ Return a string div given a toc list. """
+ div = etree.Element("div")
+ div.attrib["class"] = "toc"
+
+ # Add title to the div
+ if self.title:
+ header = etree.SubElement(div, "span")
+ header.attrib["class"] = "toctitle"
+ header.text = self.title
+
+ def build_etree_ul(toc_list, parent):
+ ul = etree.SubElement(parent, "ul")
+ for item in toc_list:
+ # List item link, to be inserted into the toc div
+ li = etree.SubElement(ul, "li")
+ link = etree.SubElement(li, "a")
+ link.text = item.get('name', '')
+ link.attrib["href"] = '#' + item.get('id', '')
+ if item['children']:
+ build_etree_ul(item['children'], li)
+ return ul
+
+ build_etree_ul(toc_list, div)
+
+ if 'prettify' in self.md.treeprocessors:
+ self.md.treeprocessors['prettify'].run(div)
+
+ return div
+
+ def run(self, doc):
+ # Get a list of id attributes
+ used_ids = set()
+ for el in doc.iter():
+ if "id" in el.attrib:
+ used_ids.add(el.attrib["id"])
+
+ toc_tokens = []
+ for el in doc.iter():
+ if isinstance(el.tag, str) and self.header_rgx.match(el.tag):
+ self.set_level(el)
+ text = get_name(el)
+
+ # Do not override pre-existing ids
+ if "id" not in el.attrib:
+ innertext = unescape(stashedHTML2text(text, self.md))
+ el.attrib["id"] = unique(self.slugify(innertext, self.sep), used_ids)
+
+ if int(el.tag[-1]) >= self.toc_top and int(el.tag[-1]) <= self.toc_bottom:
+ toc_tokens.append({
+ 'level': int(el.tag[-1]),
+ 'id': el.attrib["id"],
+ 'name': unescape(stashedHTML2text(
+ code_escape(el.attrib.get('data-toc-label', text)),
+ self.md, strip_entities=False
+ ))
+ })
+
+ # Remove the data-toc-label attribute as it is no longer needed
+ if 'data-toc-label' in el.attrib:
+ del el.attrib['data-toc-label']
+
+ if self.use_anchors:
+ self.add_anchor(el, el.attrib["id"])
+ if self.use_permalinks not in [False, None]:
+ self.add_permalink(el, el.attrib["id"])
+
+ toc_tokens = nest_toc_tokens(toc_tokens)
+ div = self.build_toc_div(toc_tokens)
+ if self.marker:
+ self.replace_marker(doc, div)
+
+ # serialize and attach to markdown instance.
+ toc = self.md.serializer(div)
+ for pp in self.md.postprocessors:
+ toc = pp.run(toc)
+ self.md.toc_tokens = toc_tokens
+ self.md.toc = toc
+
+
+class TocExtension(Extension):
+
+ TreeProcessorClass = TocTreeprocessor
+
+ def __init__(self, **kwargs):
+ self.config = {
+ "marker": ['[TOC]',
+ 'Text to find and replace with Table of Contents - '
+ 'Set to an empty string to disable. Defaults to "[TOC]"'],
+ "title": ["",
+ "Title to insert into TOC
- "
+ "Defaults to an empty string"],
+ "anchorlink": [False,
+ "True if header should be a self link - "
+ "Defaults to False"],
+ "anchorlink_class": ['toclink',
+ 'CSS class(es) used for the link. '
+ 'Defaults to "toclink"'],
+ "permalink": [0,
+ "True or link text if a Sphinx-style permalink should "
+ "be added - Defaults to False"],
+ "permalink_class": ['headerlink',
+ 'CSS class(es) used for the link. '
+ 'Defaults to "headerlink"'],
+ "permalink_title": ["Permanent link",
+ "Title attribute of the permalink - "
+ "Defaults to 'Permanent link'"],
+ "baselevel": ['1', 'Base level for headers.'],
+ "slugify": [slugify,
+ "Function to generate anchors based on header text - "
+ "Defaults to the headerid ext's slugify function."],
+ 'separator': ['-', 'Word separator. Defaults to "-".'],
+ "toc_depth": [6,
+ 'Define the range of section levels to include in'
+ 'the Table of Contents. A single integer (b) defines'
+ 'the bottom section level (