Use Ruff and fix all its warnings

This commit is contained in:
2023-11-02 00:08:42 +01:00
parent df59c33f9b
commit 2165dd5b7b
19 changed files with 347 additions and 203 deletions

61
.pre-commit-config.yaml Normal file
View File

@ -0,0 +1,61 @@
default_language_version:
python: python3.12
repos:
# Apply a consistent format to pyproject.toml files.
# https://pyproject-fmt.readthedocs.io/en/latest/
- repo: https://github.com/tox-dev/pyproject-fmt
rev: "1.4.0"
hooks:
- id: pyproject-fmt
- repo: https://github.com/asottile/add-trailing-comma
rev: v3.1.0
hooks:
- id: add-trailing-comma
# Some out-of-the-box hooks for pre-commit.
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.5.0
hooks:
- id: check-ast
- id: check-builtin-literals
- id: check-docstring-first
- id: check-executables-have-shebangs
- id: check-merge-conflict
- id: check-toml
- id: check-vcs-permalinks
- id: end-of-file-fixer
- id: mixed-line-ending
- id: name-tests-test
args: [--pytest-test-first]
- id: trailing-whitespace
args: [--markdown-linebreak-ext=md]
exclude_types:
- "html"
# Run Pyupgrade on all Python files. This will upgrade the code to Python 3.12.
- repo: https://github.com/asottile/pyupgrade
rev: v3.15.0
hooks:
- id: pyupgrade
args: ["--py312-plus"]
# An extremely fast Python linter and formatter.
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.1.3
hooks:
- id: ruff-format
- id: ruff
args: ["--fix", "--exit-non-zero-on-fix"]
# Static checker for GitHub Actions workflow files.
- repo: https://github.com/rhysd/actionlint
rev: v1.6.26
hooks:
- id: actionlint
# Optimize .png files.
- repo: https://github.com/shssoichiro/oxipng
rev: v9.0.0
hooks:
- id: oxipng

5
.vscode/launch.json vendored
View File

@ -1,7 +1,4 @@
{ {
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0", "version": "0.2.0",
"configurations": [ "configurations": [
{ {
@ -17,4 +14,4 @@
"justMyCode": true "justMyCode": true
} }
] ]
} }

View File

@ -16,7 +16,7 @@ ENV PYTHONUNBUFFERED 1
# Update the system and install curl, it is needed for downloading Poetry. # Update the system and install curl, it is needed for downloading Poetry.
RUN apt-get update && apt-get install curl ffmpeg -y --no-install-recommends RUN apt-get update && apt-get install curl ffmpeg -y --no-install-recommends
# 1. Create user so we don't run as root # 1. Create user so we don't run as root
# 2. Create directories that the bot needs that are owned by the user. # 2. Create directories that the bot needs that are owned by the user.
# /Uploads is used to store the uploaded files. # /Uploads is used to store the uploaded files.
# /home/botuser/discord-embed is where the Python code is stored. # /home/botuser/discord-embed is where the Python code is stored.
@ -45,4 +45,4 @@ VOLUME ["/Uploads"]
# Run the server on all interfaces and on port 5000. # Run the server on all interfaces and on port 5000.
# You should run a reverse proxy like nginx infront of this. # You should run a reverse proxy like nginx infront of this.
EXPOSE 5000 EXPOSE 5000
CMD ["poetry", "run", "uvicorn", "discord_embed.main:app", "--host", "0.0.0.0", "--port", "5000"] CMD ["poetry", "run", "uvicorn", "discord_embed.main:app", "--host", "0.0.0.0", "--port", "5000"]

View File

@ -1 +0,0 @@
__version__ = "1.0.0"

View File

@ -1,11 +1,19 @@
import os from __future__ import annotations
from datetime import datetime
import datetime
from pathlib import Path
from urllib.parse import urljoin from urllib.parse import urljoin
from discord_embed import settings from discord_embed import settings
def generate_html_for_videos(url: str, width: int, height: int, screenshot: str, filename: str) -> str: def generate_html_for_videos(
url: str,
width: int,
height: int,
screenshot: str,
filename: str,
) -> str:
"""Generate HTML for video files. """Generate HTML for video files.
Args: Args:
@ -18,10 +26,13 @@ def generate_html_for_videos(url: str, width: int, height: int, screenshot: str,
Returns: Returns:
Returns HTML for video. Returns HTML for video.
""" """
time_now: datetime.datetime = datetime.datetime.now(tz=datetime.UTC)
time_now_str: str = time_now.strftime("%Y-%m-%d %H:%M:%S %Z")
video_html: str = f""" video_html: str = f"""
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<!-- Generated at {datetime.now().strftime('%Y-%m-%d %H:%M:%S')} --> <!-- Generated at {time_now_str} -->
<head> <head>
<meta property="og:type" content="video.other"> <meta property="og:type" content="video.other">
<meta property="twitter:player" content="{url}"> <meta property="twitter:player" content="{url}">
@ -39,8 +50,8 @@ def generate_html_for_videos(url: str, width: int, height: int, screenshot: str,
# Take the filename and append .html to it. # Take the filename and append .html to it.
filename += ".html" filename += ".html"
file_path: str = os.path.join(settings.upload_folder, filename) file_path = Path(settings.upload_folder, filename)
with open(file_path, "w", encoding="utf-8") as f: with Path.open(file_path, "w", encoding="utf-8") as f:
f.write(video_html) f.write(video_html)
return html_url return html_url

View File

@ -1,8 +1,10 @@
from __future__ import annotations
from pathlib import Path
from urllib.parse import urljoin from urllib.parse import urljoin
from fastapi import FastAPI, File, Request, UploadFile from fastapi import FastAPI, File, Request, UploadFile
from fastapi.responses import HTMLResponse, JSONResponse from fastapi.responses import HTMLResponse, JSONResponse
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates from fastapi.templating import Jinja2Templates
from discord_embed import settings from discord_embed import settings
@ -17,12 +19,11 @@ app: FastAPI = FastAPI(
}, },
) )
app.mount("/static", StaticFiles(directory="static"), name="static")
templates: Jinja2Templates = Jinja2Templates(directory="templates") templates: Jinja2Templates = Jinja2Templates(directory="templates")
@app.post("/uploadfiles/", description="Where to send a POST request to upload files.") @app.post("/uploadfiles/", description="Where to send a POST request to upload files.")
async def upload_file(file: UploadFile = File()): async def upload_file(file: UploadFile = File()): # noqa: B008
"""Page for uploading files. """Page for uploading files.
If it is a video, we need to make an HTML file, and a thumbnail If it is a video, we need to make an HTML file, and a thumbnail
@ -35,15 +36,22 @@ async def upload_file(file: UploadFile = File()):
Returns: Returns:
Returns a dict with the filename, or a link to the .html if it was a video. Returns a dict with the filename, or a link to the .html if it was a video.
""" """
if file.filename is None:
send_webhook("Filename is None")
return JSONResponse(content={"error": "Filename is None"}, status_code=500)
if file.content_type is None:
send_webhook("Content type is None")
return JSONResponse(content={"error": "Content type is None"}, status_code=500)
if file.content_type.startswith("video/"): if file.content_type.startswith("video/"):
html_url: str = await do_things(file) html_url: str = await do_things(file)
else: else:
filename: str = await remove_illegal_chars(file.filename) filename: str = await remove_illegal_chars(file.filename)
with open(f"{settings.upload_folder}/{filename}", "wb+") as f: with Path.open(Path(settings.upload_folder, filename), "wb+") as f:
f.write(file.file.read()) f.write(file.file.read())
html_url: str = urljoin(settings.serve_domain, filename) # type: ignore html_url: str = urljoin(settings.serve_domain, filename)
send_webhook(f"{html_url} was uploaded.") send_webhook(f"{html_url} was uploaded.")
return JSONResponse(content={"html_url": html_url}) return JSONResponse(content={"html_url": html_url})
@ -58,8 +66,7 @@ async def remove_illegal_chars(file_name: str) -> str:
Returns: Returns:
Returns a string with the filename without illegal characters. Returns a string with the filename without illegal characters.
""" """
filename: str = file_name.replace(" ", ".")
filename: str = file_name.replace(" ", ".") # type: ignore
illegal_characters: list[str] = [ illegal_characters: list[str] = [
"*", "*",
'"', '"',
@ -84,13 +91,31 @@ async def remove_illegal_chars(file_name: str) -> str:
",", ",",
] ]
for character in illegal_characters: for character in illegal_characters:
filename: str = filename.replace(character, "") # type: ignore filename: str = filename.replace(character, "")
return filename return filename
index_html: str = """
<html lang="en">
<body>
<h1>discord-nice-embed</h1>
<a href="/docs">Swagger UI - API documentation</a>
<br />
<a href="/redoc">ReDoc - Alternative API documentation</a>
<form action="/uploadfiles/" enctype="multipart/form-data" method="post">
<input name="file" type="file" />
<input type="submit" value="Upload file" />
</form>
</body>
</html>
"""
@app.get("/", response_class=HTMLResponse, include_in_schema=False) @app.get("/", response_class=HTMLResponse, include_in_schema=False)
async def main(request: Request): async def main(request: Request): # noqa: ARG001
"""Our index view. """Our index view.
You can upload files here. You can upload files here.
@ -99,7 +124,6 @@ async def main(request: Request):
request: Our request. request: Our request.
Returns: Returns:
TemplateResponse: Returns HTML for site. HTMLResponse: Our index.html page.
""" """
return index_html
return templates.TemplateResponse("index.html", {"request": request})

View File

@ -1,37 +1,14 @@
import os from __future__ import annotations
import pathlib
import sys
from dotenv import load_dotenv import os
from pathlib import Path
from dotenv import find_dotenv, load_dotenv
# Load environment variables # Load environment variables
load_dotenv() load_dotenv(find_dotenv(), verbose=True)
# Check if user has added a domain to the environment. webhook_url: str = os.environ["WEBHOOK_URL"]
try: serve_domain: str = os.environ["SERVE_DOMAIN"].removesuffix("/")
serve_domain: str = os.environ["SERVE_DOMAIN"] upload_folder: str = os.environ["UPLOAD_FOLDER"].removesuffix("/")
except KeyError: Path(upload_folder).mkdir(parents=True, exist_ok=True)
sys.exit("discord-embed: Environment variable 'SERVE_DOMAIN' is missing!")
# Remove trailing slash from domain
if serve_domain.endswith("/"):
serve_domain = serve_domain[:-1]
# Check if we have a folder for uploads.
try:
upload_folder: str = os.environ["UPLOAD_FOLDER"]
except KeyError:
sys.exit("Environment variable 'UPLOAD_FOLDER' is missing!")
# Create upload_folder if it doesn't exist.
pathlib.Path(upload_folder).mkdir(parents=True, exist_ok=True)
# Remove trailing slash from upload_folder
if upload_folder.endswith("/"):
upload_folder = upload_folder[:-1]
# Discord webhook URL
try:
webhook_url: str = os.environ["WEBHOOK_URL"]
except KeyError:
sys.exit("Environment variable 'WEBHOOK_URL' is missing!")

View File

@ -1,3 +1,6 @@
from __future__ import annotations
import logging
import sys import sys
from dataclasses import dataclass from dataclasses import dataclass
@ -5,6 +8,8 @@ import ffmpeg
from discord_embed import settings from discord_embed import settings
logger: logging.Logger = logging.getLogger(__name__)
@dataclass @dataclass
class Resolution: class Resolution:
@ -28,9 +33,12 @@ def video_resolution(path_to_video: str) -> Resolution:
Returns height and width. Returns height and width.
""" """
probe = ffmpeg.probe(path_to_video) probe = ffmpeg.probe(path_to_video)
video_stream = next((stream for stream in probe["streams"] if stream["codec_type"] == "video"), None) video_stream = next(
(stream for stream in probe["streams"] if stream["codec_type"] == "video"),
None,
)
if video_stream is None: if video_stream is None:
print("No video stream found", file=sys.stderr) logger.critical("No video stream found")
sys.exit(1) sys.exit(1)
width: int = int(video_stream["width"]) width: int = int(video_stream["width"])

View File

@ -1,13 +1,16 @@
import os from __future__ import annotations
from dataclasses import dataclass from dataclasses import dataclass
from pathlib import Path from pathlib import Path
from typing import TYPE_CHECKING
from fastapi import UploadFile
from discord_embed import settings from discord_embed import settings
from discord_embed.generate_html import generate_html_for_videos from discord_embed.generate_html import generate_html_for_videos
from discord_embed.video import Resolution, make_thumbnail, video_resolution from discord_embed.video import Resolution, make_thumbnail, video_resolution
if TYPE_CHECKING:
from fastapi import UploadFile
@dataclass @dataclass
class VideoFile: class VideoFile:
@ -32,19 +35,23 @@ def save_to_disk(file: UploadFile) -> VideoFile:
Returns: Returns:
VideoFile object with the filename and location. VideoFile object with the filename and location.
""" """
if file.filename is None:
msg = "Filename is None"
raise ValueError(msg)
# Create the folder where we should save the files # Create the folder where we should save the files
folder_video: str = os.path.join(settings.upload_folder, "video") save_folder_video = Path(settings.upload_folder, "video")
Path(folder_video).mkdir(parents=True, exist_ok=True) Path(save_folder_video).mkdir(parents=True, exist_ok=True)
# Replace spaces with dots in the filename. # Replace spaces with dots in the filename.
filename: str = file.filename.replace(" ", ".") filename: str = file.filename.replace(" ", ".")
# Save the uploaded file to disk. # Save the uploaded file to disk.
file_location: str = os.path.join(folder_video, filename) file_location = Path(save_folder_video, filename)
with open(file_location, "wb+") as f: with Path.open(file_location, "wb+") as f:
f.write(file.file.read()) f.write(file.file.read())
return VideoFile(filename, file_location) return VideoFile(filename, str(file_location))
async def do_things(file: UploadFile) -> str: async def do_things(file: UploadFile) -> str:
@ -56,7 +63,6 @@ async def do_things(file: UploadFile) -> str:
Returns: Returns:
Returns URL for video. Returns URL for video.
""" """
video_file: VideoFile = save_to_disk(file) video_file: VideoFile = save_to_disk(file)
file_url: str = f"{settings.serve_domain}/video/{video_file.filename}" file_url: str = f"{settings.serve_domain}/video/{video_file.filename}"

View File

@ -1,7 +1,17 @@
from __future__ import annotations
import logging
from typing import TYPE_CHECKING
from discord_webhook import DiscordWebhook from discord_webhook import DiscordWebhook
from discord_embed import settings from discord_embed import settings
if TYPE_CHECKING:
from requests import Response
logger: logging.Logger = logging.getLogger(__name__)
def send_webhook(message: str) -> None: def send_webhook(message: str) -> None:
"""Send a webhook to Discord. """Send a webhook to Discord.
@ -11,7 +21,9 @@ def send_webhook(message: str) -> None:
""" """
webhook: DiscordWebhook = DiscordWebhook( webhook: DiscordWebhook = DiscordWebhook(
url=settings.webhook_url, url=settings.webhook_url,
content=message, content=message or "discord-nice-embed: No message was provided.",
rate_limit_retry=True, rate_limit_retry=True,
) )
webhook.execute() response: Response = webhook.execute()
if not response.ok:
logger.critical("Webhook failed to send\n %s\n %s", response, message)

View File

@ -17,4 +17,4 @@ server {
set $upstream_proto http; set $upstream_proto http;
proxy_pass $upstream_proto://$upstream_app:$upstream_port; proxy_pass $upstream_proto://$upstream_app:$upstream_port;
} }
} }

137
poetry.lock generated
View File

@ -42,6 +42,17 @@ files = [
{file = "certifi-2023.7.22.tar.gz", hash = "sha256:539cc1d13202e33ca466e88b2807e29f4c13049d6d87031a3c110744495cb082"}, {file = "certifi-2023.7.22.tar.gz", hash = "sha256:539cc1d13202e33ca466e88b2807e29f4c13049d6d87031a3c110744495cb082"},
] ]
[[package]]
name = "cfgv"
version = "3.4.0"
description = "Validate configuration and produce human readable error messages."
optional = false
python-versions = ">=3.8"
files = [
{file = "cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9"},
{file = "cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560"},
]
[[package]] [[package]]
name = "charset-normalizer" name = "charset-normalizer"
version = "3.3.2" version = "3.3.2"
@ -183,6 +194,17 @@ requests = ">=2.28.1,<3.0.0"
[package.extras] [package.extras]
async = ["httpx (>=0.23.0,<0.24.0)"] async = ["httpx (>=0.23.0,<0.24.0)"]
[[package]]
name = "distlib"
version = "0.3.7"
description = "Distribution utilities"
optional = false
python-versions = "*"
files = [
{file = "distlib-0.3.7-py2.py3-none-any.whl", hash = "sha256:2e24928bc811348f0feb63014e97aaae3037f2cf48712d51ae61df7fd6075057"},
{file = "distlib-0.3.7.tar.gz", hash = "sha256:9dafe54b34a028eafd95039d5e5d4851a13734540f1331060d31c9916e7147a8"},
]
[[package]] [[package]]
name = "fastapi" name = "fastapi"
version = "0.104.1" version = "0.104.1"
@ -220,6 +242,22 @@ future = "*"
[package.extras] [package.extras]
dev = ["Sphinx (==2.1.0)", "future (==0.17.1)", "numpy (==1.16.4)", "pytest (==4.6.1)", "pytest-mock (==1.10.4)", "tox (==3.12.1)"] dev = ["Sphinx (==2.1.0)", "future (==0.17.1)", "numpy (==1.16.4)", "pytest (==4.6.1)", "pytest-mock (==1.10.4)", "tox (==3.12.1)"]
[[package]]
name = "filelock"
version = "3.13.1"
description = "A platform independent file lock."
optional = false
python-versions = ">=3.8"
files = [
{file = "filelock-3.13.1-py3-none-any.whl", hash = "sha256:57dbda9b35157b05fb3e58ee91448612eb674172fab98ee235ccb0b5bee19a1c"},
{file = "filelock-3.13.1.tar.gz", hash = "sha256:521f5f56c50f8426f5e03ad3b281b490a87ef15bc6c526f168290f0c7148d44e"},
]
[package.extras]
docs = ["furo (>=2023.9.10)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.24)"]
testing = ["covdefaults (>=2.3)", "coverage (>=7.3.2)", "diff-cover (>=8)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)", "pytest-timeout (>=2.2)"]
typing = ["typing-extensions (>=4.8)"]
[[package]] [[package]]
name = "future" name = "future"
version = "0.18.3" version = "0.18.3"
@ -333,6 +371,20 @@ cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"]
http2 = ["h2 (>=3,<5)"] http2 = ["h2 (>=3,<5)"]
socks = ["socksio (==1.*)"] socks = ["socksio (==1.*)"]
[[package]]
name = "identify"
version = "2.5.31"
description = "File identification library for Python"
optional = false
python-versions = ">=3.8"
files = [
{file = "identify-2.5.31-py2.py3-none-any.whl", hash = "sha256:90199cb9e7bd3c5407a9b7e81b4abec4bb9d249991c79439ec8af740afc6293d"},
{file = "identify-2.5.31.tar.gz", hash = "sha256:7736b3c7a28233637e3c36550646fc6389bedd74ae84cb788200cc8e2dd60b75"},
]
[package.extras]
license = ["ukkonen"]
[[package]] [[package]]
name = "idna" name = "idna"
version = "3.4" version = "3.4"
@ -441,6 +493,20 @@ files = [
{file = "MarkupSafe-2.1.3.tar.gz", hash = "sha256:af598ed32d6ae86f1b747b82783958b1a4ab8f617b06fe68795c7f026abbdcad"}, {file = "MarkupSafe-2.1.3.tar.gz", hash = "sha256:af598ed32d6ae86f1b747b82783958b1a4ab8f617b06fe68795c7f026abbdcad"},
] ]
[[package]]
name = "nodeenv"
version = "1.8.0"
description = "Node.js virtual environment builder"
optional = false
python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*"
files = [
{file = "nodeenv-1.8.0-py2.py3-none-any.whl", hash = "sha256:df865724bb3c3adc86b3876fa209771517b0cfe596beff01a92700e0e8be4cec"},
{file = "nodeenv-1.8.0.tar.gz", hash = "sha256:d51e0c37e64fbf47d017feac3145cdbb58836d7eee8c6f6d3b6880c5456227d2"},
]
[package.dependencies]
setuptools = "*"
[[package]] [[package]]
name = "packaging" name = "packaging"
version = "23.2" version = "23.2"
@ -452,6 +518,21 @@ files = [
{file = "packaging-23.2.tar.gz", hash = "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5"}, {file = "packaging-23.2.tar.gz", hash = "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5"},
] ]
[[package]]
name = "platformdirs"
version = "3.11.0"
description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
optional = false
python-versions = ">=3.7"
files = [
{file = "platformdirs-3.11.0-py3-none-any.whl", hash = "sha256:e9d171d00af68be50e9202731309c4e658fd8bc76f55c11c7dd760d023bda68e"},
{file = "platformdirs-3.11.0.tar.gz", hash = "sha256:cf8ee52a3afdb965072dcc652433e0c7e3e40cf5ea1477cd4b3b1d2eb75495b3"},
]
[package.extras]
docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.1)", "sphinx-autodoc-typehints (>=1.24)"]
test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4)", "pytest-cov (>=4.1)", "pytest-mock (>=3.11.1)"]
[[package]] [[package]]
name = "pluggy" name = "pluggy"
version = "1.3.0" version = "1.3.0"
@ -467,6 +548,24 @@ files = [
dev = ["pre-commit", "tox"] dev = ["pre-commit", "tox"]
testing = ["pytest", "pytest-benchmark"] testing = ["pytest", "pytest-benchmark"]
[[package]]
name = "pre-commit"
version = "3.5.0"
description = "A framework for managing and maintaining multi-language pre-commit hooks."
optional = false
python-versions = ">=3.8"
files = [
{file = "pre_commit-3.5.0-py2.py3-none-any.whl", hash = "sha256:841dc9aef25daba9a0238cd27984041fa0467b4199fc4852e27950664919f660"},
{file = "pre_commit-3.5.0.tar.gz", hash = "sha256:5804465c675b659b0862f07907f96295d490822a450c4c40e747d0b1c6ebcb32"},
]
[package.dependencies]
cfgv = ">=2.0.0"
identify = ">=1.0.0"
nodeenv = ">=0.11.1"
pyyaml = ">=5.1"
virtualenv = ">=20.10.0"
[[package]] [[package]]
name = "pydantic" name = "pydantic"
version = "2.4.2" version = "2.4.2"
@ -732,6 +831,22 @@ urllib3 = ">=1.21.1,<3"
socks = ["PySocks (>=1.5.6,!=1.5.7)"] socks = ["PySocks (>=1.5.6,!=1.5.7)"]
use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"]
[[package]]
name = "setuptools"
version = "68.2.2"
description = "Easily download, build, install, upgrade, and uninstall Python packages"
optional = false
python-versions = ">=3.8"
files = [
{file = "setuptools-68.2.2-py3-none-any.whl", hash = "sha256:b454a35605876da60632df1a60f736524eb73cc47bbc9f3f1ef1b644de74fd2a"},
{file = "setuptools-68.2.2.tar.gz", hash = "sha256:4ac1475276d2f1c48684874089fefcd83bd7162ddaafb81fac866ba0db282a87"},
]
[package.extras]
docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-hoverxref (<2)", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"]
testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-ruff", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"]
testing-integration = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "packaging (>=23.1)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"]
[[package]] [[package]]
name = "sniffio" name = "sniffio"
version = "1.3.0" version = "1.3.0"
@ -857,6 +972,26 @@ files = [
docs = ["Sphinx (>=4.1.2,<4.2.0)", "sphinx-rtd-theme (>=0.5.2,<0.6.0)", "sphinxcontrib-asyncio (>=0.3.0,<0.4.0)"] docs = ["Sphinx (>=4.1.2,<4.2.0)", "sphinx-rtd-theme (>=0.5.2,<0.6.0)", "sphinxcontrib-asyncio (>=0.3.0,<0.4.0)"]
test = ["Cython (>=0.29.36,<0.30.0)", "aiohttp (==3.9.0b0)", "aiohttp (>=3.8.1)", "flake8 (>=5.0,<6.0)", "mypy (>=0.800)", "psutil", "pyOpenSSL (>=23.0.0,<23.1.0)", "pycodestyle (>=2.9.0,<2.10.0)"] test = ["Cython (>=0.29.36,<0.30.0)", "aiohttp (==3.9.0b0)", "aiohttp (>=3.8.1)", "flake8 (>=5.0,<6.0)", "mypy (>=0.800)", "psutil", "pyOpenSSL (>=23.0.0,<23.1.0)", "pycodestyle (>=2.9.0,<2.10.0)"]
[[package]]
name = "virtualenv"
version = "20.24.6"
description = "Virtual Python Environment builder"
optional = false
python-versions = ">=3.7"
files = [
{file = "virtualenv-20.24.6-py3-none-any.whl", hash = "sha256:520d056652454c5098a00c0f073611ccbea4c79089331f60bf9d7ba247bb7381"},
{file = "virtualenv-20.24.6.tar.gz", hash = "sha256:02ece4f56fbf939dbbc33c0715159951d6bf14aaf5457b092e4548e1382455af"},
]
[package.dependencies]
distlib = ">=0.3.7,<1"
filelock = ">=3.12.2,<4"
platformdirs = ">=3.9.1,<4"
[package.extras]
docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"]
test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8)", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10)"]
[[package]] [[package]]
name = "watchfiles" name = "watchfiles"
version = "0.21.0" version = "0.21.0"
@ -1028,4 +1163,4 @@ files = [
[metadata] [metadata]
lock-version = "2.0" lock-version = "2.0"
python-versions = "^3.12" python-versions = "^3.12"
content-hash = "3eb51d6b10b9c56043cfeaf3cffbe0f30f526ff774b2c4bbd7672f5e3a0d6b03" content-hash = "939a371df3b2147c6a306b152b4f7011fc0663defba72c79085f705b0eaeef9c"

View File

@ -18,16 +18,20 @@ uvicorn = { extras = ["standard"], version = "^0.23.0" }
[tool.poetry.group.dev.dependencies] [tool.poetry.group.dev.dependencies]
httpx = "^0.25.0" httpx = "^0.25.0"
pytest = "^7.4.3" pytest = "^7.4.3"
pre-commit = "^3.5.0"
[build-system] [build-system]
requires = ["poetry-core>=1.0.0"]
build-backend = "poetry.core.masonry.api" build-backend = "poetry.core.masonry.api"
requires = [
"poetry-core>=1",
]
[tool.ruff] [tool.ruff]
fix = true fix = true
unsafe-fixes = true
preview = true preview = true
select = ["ALL"] select = ["ALL"]
ignore = ["D100", "CPY001"] ignore = ["D100", "D104", "CPY001", "ANN201"]
[tool.ruff.pydocstyle] [tool.ruff.pydocstyle]
convention = "google" convention = "google"

View File

@ -1,12 +0,0 @@
body {
background-color: #111111;
color: #DCDDDE;
}
a:link {
color: #00AFF4;
}
a:visited {
color: #00AFF4;
}

View File

@ -1,21 +1,14 @@
<!DOCTYPE html>
<html lang="en"> <html lang="en">
<head>
<title>discord-nice-embed</title>
<meta content="text/html; charset=utf-8"/>
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
<link href="static/style.css" rel="stylesheet"/>
</head>
<body> <body>
<h1>discord-nice-embed</h1> <h1>discord-nice-embed</h1>
<a href="/docs">Swagger UI - API documentation</a> <a href="/docs">Swagger UI - API documentation</a>
<br/> <br />
<a href="/redoc">ReDoc - Alternative API documentation</a> <a href="/redoc">ReDoc - Alternative API documentation</a>
<br/> <form action="/uploadfiles/" enctype="multipart/form-data" method="post">
<br/> <input name="file" type="file" />
<form action="/uploadfiles/" enctype="multipart/form-data" method="post"> <input type="submit" value="Upload file" />
<input name="file" type="file"/> </form>
<input type="submit" value="Upload file"/>
</form>
</body> </body>
</html> </html>

View File

@ -1,68 +1,44 @@
import os from __future__ import annotations
import os
from pathlib import Path
from typing import TYPE_CHECKING
from fastapi import Response
from fastapi.testclient import TestClient from fastapi.testclient import TestClient
from discord_embed import __version__, settings from discord_embed import settings
from discord_embed.main import app from discord_embed.main import app
if TYPE_CHECKING:
import httpx
client: TestClient = TestClient(app) client: TestClient = TestClient(app)
TEST_FILE: str = "tests/test.mp4" TEST_FILE: str = "tests/test.mp4"
def test_version() -> None:
"""Test version is correct."""
assert __version__ == "1.0.0"
def test_domain_ends_with_slash() -> None: def test_domain_ends_with_slash() -> None:
"""Test domain ends with a slash.""" """Test domain ends with a slash."""
assert not settings.serve_domain.endswith("/") assert not settings.serve_domain.endswith("/")
def test_save_to_disk() -> None:
"""Test save_to_disk() works."""
# TODO: Implement this test. I need to mock the UploadFile object.
def test_do_things() -> None:
"""Test do_things() works."""
# TODO: Implement this test. I need to mock the UploadFile object.
def test_main() -> None: def test_main() -> None:
"""Test main() works.""" """Test main() works."""
data_without_trailing_nl = "" response: httpx.Response = client.get("/")
response: Response = client.get("/") assert response.is_success
# Check if response is our HTML.
with open("templates/index.html", encoding="utf8") as our_html:
data: str = our_html.read()
# index.html has a trailing newline that we need to remove.
if data[-1:] == "\n":
data_without_trailing_nl: str = data[:-1] # type: ignore
assert response.status_code == 200
assert response.text == data_without_trailing_nl
def test_upload_file() -> None: def test_upload_file() -> None:
"""Test if we can upload files.""" """Test if we can upload files."""
domain = os.environ["SERVE_DOMAIN"] domain = os.environ["SERVE_DOMAIN"].removesuffix("/")
# Remove trailing slash from domain
if domain.endswith("/"):
domain: str = domain[:-1] # type: ignore
# Upload our video file and check if it returns the html_url. # Upload our video file and check if it returns the html_url.
with open(TEST_FILE, "rb") as uploaded_file: with Path.open(Path(TEST_FILE), "rb") as uploaded_file:
response: Response = client.post( response: httpx.Response = client.post(
url="/uploadfiles/", url="/uploadfiles/",
files={"file": uploaded_file}, files={"file": uploaded_file},
) )
returned_json = response.json() returned_json = response.json()
html_url: str = returned_json["html_url"] html_url: str = returned_json["html_url"]
assert response.status_code == 200 assert response.is_success
assert html_url == f"{domain}/test.mp4" assert html_url == f"{domain}/test.mp4"

View File

@ -1,4 +1,7 @@
from __future__ import annotations
import os import os
from pathlib import Path
from discord_embed.generate_html import generate_html_for_videos from discord_embed.generate_html import generate_html_for_videos
@ -12,8 +15,8 @@ def test_generate_html_for_videos() -> None:
domain = domain[:-1] domain = domain[:-1]
# Delete the old HTML file if it exists # Delete the old HTML file if it exists
if os.path.exists("Uploads/test_video.mp4.html"): if Path.exists(Path("Uploads/test_video.mp4.html")):
os.remove("Uploads/test_video.mp4.html") Path.unlink(Path("Uploads/test_video.mp4.html"))
generated_html: str = generate_html_for_videos( generated_html: str = generate_html_for_videos(
url="https://www.youtube.com/watch?v=dQw4w9WgXcQ", url="https://www.youtube.com/watch?v=dQw4w9WgXcQ",
@ -23,58 +26,3 @@ def test_generate_html_for_videos() -> None:
filename="test_video.mp4", filename="test_video.mp4",
) )
assert generated_html == f"{domain}/test_video.mp4" assert generated_html == f"{domain}/test_video.mp4"
# Open the generated HTML and check if it contains the correct URL, width, height, and screenshot.
with open("Uploads/test_video.mp4.html", "r") as generated_html_file:
generated_html_lines: list[str] = generated_html_file.readlines()
"""
<!DOCTYPE html>
<html>
<!-- Generated at 2022-08-08 08:16:53 -->
<head>
<meta property="og:type" content="video.other">
<meta property="twitter:player" content="https://www.youtube.com/watch?v=dQw4w9WgXcQ">
<meta property="og:video:type" content="text/html">
<meta property="og:video:width" content="1920">
<meta property="og:video:height" content="1080">
<meta name="twitter:image" content="https://i.ytimg.com/vi/dQw4w9WgXcQ/hqdefault.jpg">
<meta http-equiv="refresh" content="0;url=https://www.youtube.com/watch?v=dQw4w9WgXcQ">
</head>
</html>
"""
for line, html in enumerate(generated_html_lines):
# Strip spaces and newlines
stripped_html: str = html.strip()
rick: str = "https://www.youtube.com/watch?v=dQw4w9WgXcQ"
# Check each line
if line == 1:
assert stripped_html == "<!DOCTYPE html>"
elif line == 2:
assert stripped_html == "<html>"
elif line == 3:
assert stripped_html.startswith("<!-- Generated at ")
elif line == 4:
assert stripped_html == "<head>"
elif line == 5:
assert stripped_html == '<meta property="og:type" content="video.other">'
elif line == 6:
assert stripped_html == f'<meta property="twitter:player" content="{rick}">'
elif line == 7:
assert stripped_html == '<meta property="og:video:type" content="text/html">'
elif line == 8:
assert stripped_html == '<meta property="og:video:width" content="1920">'
elif line == 9:
assert stripped_html == '<meta property="og:video:height" content="1080">'
elif line == 10:
thumb: str = "https://i.ytimg.com/vi/dQw4w9WgXcQ/hqdefault.jpg"
assert stripped_html == f'<meta name="twitter:image" content="{thumb}">'
elif line == 11:
assert stripped_html == f'<meta http-equiv="refresh" content="0;url={rick}">'
elif line == 12:
assert stripped_html == "</head>"
elif line == 13:
assert stripped_html == "</html>"

View File

@ -1,5 +1,8 @@
from __future__ import annotations
import imghdr import imghdr
import os import os
from pathlib import Path
from discord_embed import settings from discord_embed import settings
from discord_embed.video import Resolution, make_thumbnail, video_resolution from discord_embed.video import Resolution, make_thumbnail, video_resolution
@ -18,11 +21,11 @@ def test_make_thumbnail() -> None:
# Remove trailing slash from domain # Remove trailing slash from domain
if domain.endswith("/"): if domain.endswith("/"):
domain: str = domain[:-1] # type: ignore domain: str = domain[:-1]
# Remove thumbnail if it exists # Remove thumbnail if it exists
if os.path.exists(f"{settings.upload_folder}/test.mp4.jpg"): if Path.exists(Path(f"{settings.upload_folder}/test.mp4.jpg")):
os.remove(f"{settings.upload_folder}/test.mp4.jpg") Path.unlink(Path(f"{settings.upload_folder}/test.mp4.jpg"))
thumbnail: str = make_thumbnail(TEST_FILE, "test.mp4") thumbnail: str = make_thumbnail(TEST_FILE, "test.mp4")

View File

@ -1,3 +1,5 @@
from __future__ import annotations
from discord_embed.webhook import send_webhook from discord_embed.webhook import send_webhook