mirror of
https://github.com/TheLovinator1/wutheringwaves.git
synced 2025-11-27 20:49:48 +01:00
Compare commits
12 Commits
copilot/fi
...
49abdd8160
| Author | SHA1 | Date | |
|---|---|---|---|
| 49abdd8160 | |||
| fe3181d9a3 | |||
| 6acf030e74 | |||
| 7f102a30ed | |||
|
|
27b14909df | ||
| b2d4ad5946 | |||
| 8b505af889 | |||
| a09ea0bd9a | |||
| 192615fddb | |||
| 572b863adb | |||
| 92078ed39a | |||
| 282a5fdf75 |
17
.github/workflows/main.yml
vendored
17
.github/workflows/main.yml
vendored
@@ -2,35 +2,28 @@ name: Run Scraper
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 * * * *' # Every hour
|
||||
- cron: "0 */15 * * *" # Every 15 minutes
|
||||
workflow_dispatch:
|
||||
push:
|
||||
paths:
|
||||
- 'scrape.py'
|
||||
- '.github/workflows/main.yml'
|
||||
- "scrape.py"
|
||||
- ".github/workflows/main.yml"
|
||||
|
||||
jobs:
|
||||
scrape:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-slim
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.13'
|
||||
|
||||
- name: Install uv
|
||||
id: setup-uv
|
||||
uses: astral-sh/setup-uv@v4
|
||||
with:
|
||||
enable-cache: true
|
||||
|
||||
- name: Install dependencies
|
||||
run: uv sync
|
||||
run: uv sync --all-groups -U
|
||||
|
||||
- name: Run script
|
||||
run: uv run python scrape.py
|
||||
|
||||
220
.gitignore
vendored
220
.gitignore
vendored
@@ -1,6 +1,218 @@
|
||||
articles/ArticleMenu.json
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.pyc
|
||||
*.pyo
|
||||
*.pyd
|
||||
*.py[codz]
|
||||
*$py.class
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
share/python-wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
MANIFEST
|
||||
|
||||
# PyInstaller
|
||||
# Usually these files are written by a python script from a template
|
||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||
*.manifest
|
||||
*.spec
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.nox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
*.py.cover
|
||||
.hypothesis/
|
||||
.pytest_cache/
|
||||
cover/
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
*.pot
|
||||
|
||||
# Django stuff:
|
||||
*.log
|
||||
local_settings.py
|
||||
db.sqlite3
|
||||
db.sqlite3-journal
|
||||
|
||||
# Flask stuff:
|
||||
instance/
|
||||
.webassets-cache
|
||||
|
||||
# Scrapy stuff:
|
||||
.scrapy
|
||||
|
||||
# Sphinx documentation
|
||||
docs/_build/
|
||||
|
||||
# PyBuilder
|
||||
.pybuilder/
|
||||
target/
|
||||
|
||||
# Jupyter Notebook
|
||||
.ipynb_checkpoints
|
||||
|
||||
# IPython
|
||||
profile_default/
|
||||
ipython_config.py
|
||||
|
||||
# pyenv
|
||||
# For a library or package, you might want to ignore these files since the code is
|
||||
# intended to run in multiple environments; otherwise, check them in:
|
||||
# .python-version
|
||||
|
||||
# pipenv
|
||||
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||
# install all needed dependencies.
|
||||
# Pipfile.lock
|
||||
|
||||
# UV
|
||||
# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
|
||||
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
||||
# commonly ignored for libraries.
|
||||
uv.lock
|
||||
|
||||
# poetry
|
||||
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
||||
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
||||
# commonly ignored for libraries.
|
||||
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
||||
# poetry.lock
|
||||
# poetry.toml
|
||||
|
||||
# pdm
|
||||
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
||||
# pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python.
|
||||
# https://pdm-project.org/en/latest/usage/project/#working-with-version-control
|
||||
# pdm.lock
|
||||
# pdm.toml
|
||||
.pdm-python
|
||||
.pdm-build/
|
||||
|
||||
# pixi
|
||||
# Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control.
|
||||
# pixi.lock
|
||||
# Pixi creates a virtual environment in the .pixi directory, just like venv module creates one
|
||||
# in the .venv directory. It is recommended not to include this directory in version control.
|
||||
.pixi
|
||||
|
||||
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
||||
__pypackages__/
|
||||
|
||||
# Celery stuff
|
||||
celerybeat-schedule
|
||||
celerybeat.pid
|
||||
|
||||
# Redis
|
||||
*.rdb
|
||||
*.aof
|
||||
*.pid
|
||||
|
||||
# RabbitMQ
|
||||
mnesia/
|
||||
rabbitmq/
|
||||
rabbitmq-data/
|
||||
|
||||
# ActiveMQ
|
||||
activemq-data/
|
||||
|
||||
# SageMath parsed files
|
||||
*.sage.py
|
||||
|
||||
# Environments
|
||||
.env
|
||||
.envrc
|
||||
.venv
|
||||
env/
|
||||
venv/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
|
||||
# Spyder project settings
|
||||
.spyderproject
|
||||
.spyproject
|
||||
|
||||
# Rope project settings
|
||||
.ropeproject
|
||||
|
||||
# mkdocs documentation
|
||||
/site
|
||||
|
||||
# mypy
|
||||
.mypy_cache/
|
||||
.dmypy.json
|
||||
dmypy.json
|
||||
|
||||
# Pyre type checker
|
||||
.pyre/
|
||||
|
||||
# pytype static type analyzer
|
||||
.pytype/
|
||||
|
||||
# Cython debug symbols
|
||||
cython_debug/
|
||||
|
||||
# PyCharm
|
||||
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
||||
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
||||
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
||||
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||
# .idea/
|
||||
|
||||
# Abstra
|
||||
# Abstra is an AI-powered process automation framework.
|
||||
# Ignore directories containing user credentials, local state, and settings.
|
||||
# Learn more at https://abstra.io/docs
|
||||
.abstra/
|
||||
|
||||
# Visual Studio Code
|
||||
# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
|
||||
# that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
|
||||
# and can be added to the global gitignore or merged into this file. However, if you prefer,
|
||||
# you could uncomment the following to ignore the entire vscode folder
|
||||
# .vscode/
|
||||
|
||||
# Ruff stuff:
|
||||
.ruff_cache/
|
||||
|
||||
# PyPI configuration file
|
||||
.pypirc
|
||||
|
||||
# Marimo
|
||||
marimo/_static/
|
||||
marimo/_lsp/
|
||||
__marimo__/
|
||||
|
||||
# Streamlit
|
||||
.streamlit/secrets.toml
|
||||
|
||||
articles/ArticleMenu.json
|
||||
|
||||
6
articles_all.xml
generated
6
articles_all.xml
generated
@@ -19259,7 +19259,7 @@
|
||||
</li>
|
||||
</ol>
|
||||
<h1>Other</h1>
|
||||
<ol start="01">
|
||||
<ol>
|
||||
<li>
|
||||
<p>Increased the redemption limit in the "Item Exchange - Oscillated Coral" Store page for convene items including Radiant Tide, Forging Tide, and Lustrous Tide from 6 to 7 per version.</p>
|
||||
</li>
|
||||
@@ -19299,7 +19299,7 @@
|
||||
</ol>
|
||||
<p>Bug Fixes</p>
|
||||
<p>Resonators</p>
|
||||
<ol start="01">
|
||||
<ol>
|
||||
<li>
|
||||
<p>Fixed the issue where Yinlin's voice line did not play when opening Supply Chests.</p>
|
||||
</li>
|
||||
@@ -19360,7 +19360,7 @@
|
||||
<li>Fixed the issue where certain actions would increase the Shell Credit costs when leveling up Echoes and Resonators. To make up for any inconvenience this may have caused, we will issue Shell Credit x300,000 as compensation.</li>
|
||||
</ol>
|
||||
<p>Eligibility: Create a character and unlock the Mail function before 2024-06-28 06:00 (UTC+8). The compensation is valid until 2024-08-15 06:00 (UTC+8).</p>
|
||||
<ol start="02">
|
||||
<ol start="2">
|
||||
<li>
|
||||
<p>Fixed the issue where the Custom Markers on the map would shift abnormally in certain situations.</p>
|
||||
</li>
|
||||
|
||||
@@ -3,13 +3,67 @@ name = "wutheringwaves"
|
||||
version = "0.1.0"
|
||||
description = "Wuthering Waves archive"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.13"
|
||||
requires-python = ">=3.14"
|
||||
dependencies = [
|
||||
"aiofiles",
|
||||
"beautifulsoup4",
|
||||
"httpx",
|
||||
"markdown>=3.8",
|
||||
"markdown",
|
||||
"markdownify",
|
||||
"markupsafe",
|
||||
"mdformat",
|
||||
]
|
||||
|
||||
[tool.ruff]
|
||||
preview = true
|
||||
line-length = 160
|
||||
lint.select = ["ALL"]
|
||||
lint.pydocstyle.convention = "google"
|
||||
|
||||
lint.ignore = [
|
||||
"ANN201", # Checks that public functions and methods have return type annotations.
|
||||
"ARG001", # Checks for the presence of unused arguments in function definitions.
|
||||
"B008", # Allow Form() as a default value
|
||||
"CPY001", # Missing copyright notice at top of file
|
||||
"D100", # Checks for undocumented public module definitions.
|
||||
"D101", # Checks for undocumented public class definitions.
|
||||
"D102", # Checks for undocumented public method definitions.
|
||||
"D104", # Missing docstring in public package.
|
||||
"D105", # Missing docstring in magic method.
|
||||
"D105", # pydocstyle - missing docstring in magic method
|
||||
"D106", # Checks for undocumented public class definitions, for nested classes.
|
||||
"ERA001", # Found commented-out code
|
||||
"FBT003", # Checks for boolean positional arguments in function calls.
|
||||
"FIX002", # Line contains TODO
|
||||
"G002", # Allow % in logging
|
||||
"PGH003", # Check for type: ignore annotations that suppress all type warnings, as opposed to targeting specific type warnings.
|
||||
"PLR6301", # Checks for the presence of unused self parameter in methods definitions.
|
||||
"RUF029", # Checks for functions declared async that do not await or otherwise use features requiring the function to be declared async.
|
||||
"TD003", # Checks that a TODO comment is associated with a link to a relevant issue or ticket.
|
||||
"PLR0913", # Checks for function definitions that include too many arguments.
|
||||
"PLR0917", # Checks for function definitions that include too many positional arguments.
|
||||
"N815", # Checks for class variable names that follow the mixedCase convention.
|
||||
|
||||
# Conflicting lint rules when using Ruff's formatter
|
||||
# https://docs.astral.sh/ruff/formatter/#conflicting-lint-rules
|
||||
"COM812", # Checks for the absence of trailing commas.
|
||||
"COM819", # Checks for the presence of prohibited trailing commas.
|
||||
"D206", # Checks for docstrings that are indented with tabs.
|
||||
"D300", # Checks for docstrings that use '''triple single quotes''' instead of """triple double quotes""".
|
||||
"E111", # Checks for indentation with a non-multiple of 4 spaces.
|
||||
"E114", # Checks for indentation of comments with a non-multiple of 4 spaces.
|
||||
"E117", # Checks for over-indented code.
|
||||
"ISC001", # Checks for implicitly concatenated strings on a single line.
|
||||
"ISC002", # Checks for implicitly concatenated strings that span multiple lines.
|
||||
"Q000", # Checks for inline strings that use single quotes or double quotes, depending on the value of the lint.flake8-quotes.inline-quotes option.
|
||||
"Q001", # Checks for multiline strings that use single quotes or double quotes, depending on the value of the lint.flake8-quotes.multiline-quotes setting.
|
||||
"Q002", # Checks for docstrings that use single quotes or double quotes, depending on the value of the lint.flake8-quotes.docstring-quotes setting.
|
||||
"Q003", # Checks for strings that include escaped quotes, and suggests changing the quote style to avoid the need to escape them.
|
||||
"W191", # Checks for indentation that uses tabs.
|
||||
]
|
||||
|
||||
[tool.ruff.lint.per-file-ignores]
|
||||
"tests/*" = ["S101", "D103", "PLR2004"]
|
||||
|
||||
[tool.ruff.lint.isort]
|
||||
force-single-line = true
|
||||
|
||||
307
scrape.py
307
scrape.py
@@ -1,4 +1,4 @@
|
||||
import asyncio # noqa: CPY001, D100
|
||||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
@@ -6,57 +6,52 @@ import re
|
||||
import shutil
|
||||
import subprocess # noqa: S404
|
||||
import time
|
||||
from datetime import UTC, datetime
|
||||
from datetime import UTC
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING, Any, Literal
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import Any
|
||||
from typing import Literal
|
||||
|
||||
import aiofiles
|
||||
import anyio
|
||||
import httpx
|
||||
import markdown
|
||||
import mdformat
|
||||
from markdownify import MarkdownConverter # pyright: ignore[reportMissingTypeStubs]
|
||||
from markupsafe import Markup, escape
|
||||
from bs4 import BeautifulSoup
|
||||
from markdownify import MarkdownConverter
|
||||
from markupsafe import Markup
|
||||
from markupsafe import escape
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Coroutine
|
||||
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format="%(message)s",
|
||||
)
|
||||
logging.basicConfig(level=logging.INFO, format="%(message)s")
|
||||
|
||||
logger: logging.Logger = logging.getLogger("wutheringwaves")
|
||||
|
||||
# Compile regex patterns for better performance
|
||||
DISCORD_LINK_PATTERN = re.compile(r'\[([^\]]+)\]\((https?://[^\s)]+) "\2"\)')
|
||||
SQUARE_BRACKETS_PATTERN = re.compile(r"^\s*\[([^\]]+)\]\s*$", re.MULTILINE)
|
||||
BALL_PATTERN = re.compile(r"●\s*(.*?)\n", re.MULTILINE)
|
||||
REFERENCE_MARK_PATTERN = re.compile(r"^\s*※\s*(\S.*?)\s*$", re.MULTILINE)
|
||||
ESCAPED_STAR_PATTERN = re.compile(r"\\\*(.*)", re.MULTILINE)
|
||||
NON_BREAKING_SPACE_PATTERN = re.compile(r"[\xa0\u2002\u2003\u2009]") # Various nbsp characters
|
||||
EMPTY_CODE_BLOCK_PATTERN = re.compile(r"```[ \t]*\n[ \t]*\n```")
|
||||
DISCORD_LINK_PATTERN: re.Pattern[str] = re.compile(r'\[([^\]]+)\]\((https?://[^\s)]+) "\2"\)')
|
||||
URL_PREFIX_PATTERN: re.Pattern[str] = re.compile(r"^https?://(www\.)?")
|
||||
NON_BREAKING_SPACE_PATTERN: re.Pattern[str] = re.compile(r"\xa0")
|
||||
EMPTY_CODE_BLOCK_PATTERN: re.Pattern[str] = re.compile(r"```[ \t]*\n[ \t]*\n```")
|
||||
SQUARE_BRACKETS_PATTERN: re.Pattern[str] = re.compile(r"^\s*\[([^\]]+)\]\s*$", re.MULTILINE)
|
||||
BALL_PATTERN: re.Pattern[str] = re.compile(r"●\s*(.*?)\n", re.MULTILINE)
|
||||
REFERENCE_MARK_PATTERN: re.Pattern[str] = re.compile(r"^\s*※\s*(\S.*?)\s*$", re.MULTILINE)
|
||||
ESCAPED_STAR_PATTERN: re.Pattern[str] = re.compile(r"\\\*(.*)", re.MULTILINE)
|
||||
|
||||
# Circled number patterns - precompile for better performance
|
||||
CIRCLED_NUMBERS = {
|
||||
"①": ("1", re.compile(r"^\s*①\s*(.*?)\s*$", re.MULTILINE)),
|
||||
"②": ("2", re.compile(r"^\s*②\s*(.*?)\s*$", re.MULTILINE)),
|
||||
"③": ("3", re.compile(r"^\s*③\s*(.*?)\s*$", re.MULTILINE)),
|
||||
"④": ("4", re.compile(r"^\s*④\s*(.*?)\s*$", re.MULTILINE)),
|
||||
"⑤": ("5", re.compile(r"^\s*⑤\s*(.*?)\s*$", re.MULTILINE)),
|
||||
"⑥": ("6", re.compile(r"^\s*⑥\s*(.*?)\s*$", re.MULTILINE)),
|
||||
"⑦": ("7", re.compile(r"^\s*⑦\s*(.*?)\s*$", re.MULTILINE)),
|
||||
"⑧": ("8", re.compile(r"^\s*⑧\s*(.*?)\s*$", re.MULTILINE)),
|
||||
"⑨": ("9", re.compile(r"^\s*⑨\s*(.*?)\s*$", re.MULTILINE)),
|
||||
"⑩": ("10", re.compile(r"^\s*⑩\s*(.*?)\s*$", re.MULTILINE)),
|
||||
CIRCLED_NUMBER_PATTERNS: dict[str, tuple[re.Pattern[str], str]] = {
|
||||
"①": (re.compile(r"^\s*①\s*(.*?)\s*$", re.MULTILINE), "1"),
|
||||
"②": (re.compile(r"^\s*②\s*(.*?)\s*$", re.MULTILINE), "2"),
|
||||
"③": (re.compile(r"^\s*③\s*(.*?)\s*$", re.MULTILINE), "3"),
|
||||
"④": (re.compile(r"^\s*④\s*(.*?)\s*$", re.MULTILINE), "4"),
|
||||
"⑤": (re.compile(r"^\s*⑤\s*(.*?)\s*$", re.MULTILINE), "5"),
|
||||
"⑥": (re.compile(r"^\s*⑥\s*(.*?)\s*$", re.MULTILINE), "6"),
|
||||
"⑦": (re.compile(r"^\s*⑦\s*(.*?)\s*$", re.MULTILINE), "7"),
|
||||
"⑧": (re.compile(r"^\s*⑧\s*(.*?)\s*$", re.MULTILINE), "8"),
|
||||
"⑨": (re.compile(r"^\s*⑨\s*(.*?)\s*$", re.MULTILINE), "9"),
|
||||
"⑩": (re.compile(r"^\s*⑩\s*(.*?)\s*$", re.MULTILINE), "10"),
|
||||
}
|
||||
|
||||
# Markdown converter instance - reuse instead of creating for each article
|
||||
MARKDOWN_CONVERTER = MarkdownConverter(
|
||||
heading_style="ATX",
|
||||
strip=["pre", "code"],
|
||||
)
|
||||
|
||||
|
||||
|
||||
async def fetch_json(url: str, client: httpx.AsyncClient) -> dict[Any, Any] | None:
|
||||
"""Fetch JSON data from a URL.
|
||||
@@ -112,9 +107,7 @@ def set_file_timestamp(filepath: Path, timestamp_str: str) -> bool:
|
||||
"""
|
||||
try:
|
||||
# Parse the timestamp string
|
||||
dt: datetime = datetime.strptime(timestamp_str, "%Y-%m-%d %H:%M:%S").replace(
|
||||
tzinfo=UTC
|
||||
)
|
||||
dt: datetime = datetime.strptime(timestamp_str, "%Y-%m-%d %H:%M:%S").replace(tzinfo=UTC)
|
||||
|
||||
# Convert to Unix timestamp
|
||||
timestamp: float = dt.timestamp()
|
||||
@@ -145,9 +138,7 @@ def get_file_timestamp(timestamp_str: str) -> float:
|
||||
|
||||
try:
|
||||
# Parse the timestamp string
|
||||
dt: datetime = datetime.strptime(timestamp_str, "%Y-%m-%d %H:%M:%S").replace(
|
||||
tzinfo=UTC
|
||||
)
|
||||
dt: datetime = datetime.strptime(timestamp_str, "%Y-%m-%d %H:%M:%S").replace(tzinfo=UTC)
|
||||
# Convert to Unix timestamp
|
||||
return dt.timestamp()
|
||||
except ValueError:
|
||||
@@ -166,13 +157,7 @@ def commit_file_with_timestamp(filepath: Path) -> bool: # noqa: PLR0911
|
||||
|
||||
"""
|
||||
# Check in Git history if we already have this file
|
||||
git_log_cmd: list[str] = [
|
||||
"git",
|
||||
"log",
|
||||
"--pretty=format:%H",
|
||||
"--follow",
|
||||
str(filepath),
|
||||
]
|
||||
git_log_cmd: list[str] = ["git", "log", "--pretty=format:%H", "--follow", str(filepath)]
|
||||
try:
|
||||
git_log_output: str = subprocess.check_output(git_log_cmd, text=True).strip() # noqa: S603
|
||||
if git_log_output:
|
||||
@@ -196,25 +181,14 @@ def commit_file_with_timestamp(filepath: Path) -> bool: # noqa: PLR0911
|
||||
|
||||
# Get the file's modification time
|
||||
timestamp: float = filepath.stat().st_mtime
|
||||
git_time: str = datetime.fromtimestamp(timestamp, tz=UTC).strftime(
|
||||
"%Y-%m-%dT%H:%M:%S"
|
||||
)
|
||||
git_time: str = datetime.fromtimestamp(timestamp, tz=UTC).strftime("%Y-%m-%dT%H:%M:%S")
|
||||
|
||||
# Stage the file
|
||||
subprocess.run([git_executable, "add", str(filepath)], check=True, text=True) # noqa: S603
|
||||
|
||||
# Commit the file with the modification time as the commit time
|
||||
env: dict[str, str] = {
|
||||
**os.environ,
|
||||
"GIT_AUTHOR_DATE": git_time,
|
||||
"GIT_COMMITTER_DATE": git_time,
|
||||
}
|
||||
subprocess.run( # noqa: S603
|
||||
[git_executable, "commit", "-m", f"Add {filepath.name}"],
|
||||
check=True,
|
||||
env=env,
|
||||
text=True,
|
||||
)
|
||||
env: dict[str, str] = {**os.environ, "GIT_AUTHOR_DATE": git_time, "GIT_COMMITTER_DATE": git_time}
|
||||
subprocess.run([git_executable, "commit", "-m", f"Add {filepath.name}"], check=True, env=env, text=True) # noqa: S603
|
||||
except subprocess.CalledProcessError:
|
||||
logger.exception("Subprocess error occurred while committing the file.")
|
||||
return False
|
||||
@@ -251,9 +225,7 @@ def add_articles_to_readme(articles: dict[Any, Any] | None = None) -> None:
|
||||
# Create new content
|
||||
new_lines: list[str] = []
|
||||
if articles_section_index >= 0:
|
||||
new_lines = lines[
|
||||
: articles_section_index + 1
|
||||
] # Keep everything up to "## Articles"
|
||||
new_lines = lines[: articles_section_index + 1] # Keep everything up to "## Articles"
|
||||
else:
|
||||
new_lines = lines
|
||||
if new_lines and not new_lines[-1].endswith("\n"):
|
||||
@@ -262,17 +234,11 @@ def add_articles_to_readme(articles: dict[Any, Any] | None = None) -> None:
|
||||
|
||||
# Add articles
|
||||
new_lines.append("\n") # Add a blank line after the heading
|
||||
for article in sorted(
|
||||
articles, key=lambda x: x.get("createTime", ""), reverse=True
|
||||
):
|
||||
for article in sorted(articles, key=lambda x: x.get("createTime", ""), reverse=True):
|
||||
article_id: str = str(article.get("articleId", ""))
|
||||
article_title: str = article.get("articleTitle", "No Title")
|
||||
article_url: str = (
|
||||
f"https://wutheringwaves.kurogames.com/en/main/news/detail/{article_id}"
|
||||
)
|
||||
new_lines.append(
|
||||
f"- [{article_title}]({article_url}) [[json]](articles/{article_id}.json)\n",
|
||||
)
|
||||
article_url: str = f"https://wutheringwaves.kurogames.com/en/main/news/detail/{article_id}"
|
||||
new_lines.append(f"- [{article_title}]({article_url}) [[json]](articles/{article_id}.json)\n")
|
||||
|
||||
# Add articles directory section
|
||||
new_lines.append("\n## Articles Directory\n\n")
|
||||
@@ -349,12 +315,14 @@ def format_discord_links(md: str) -> str:
|
||||
|
||||
def repl(match: re.Match[str]) -> str:
|
||||
url: str | Any = match.group(2)
|
||||
display: str = re.sub(pattern=r"^https?://(www\.)?", repl="", string=url)
|
||||
display: str = URL_PREFIX_PATTERN.sub("", url)
|
||||
return f"[{display}]({url})"
|
||||
|
||||
# Before: [Link](https://example.com "Link")
|
||||
# After: [Link](https://example.com)
|
||||
return DISCORD_LINK_PATTERN.sub(repl, md)
|
||||
formatted_links_md: str = DISCORD_LINK_PATTERN.sub(repl, md)
|
||||
|
||||
return formatted_links_md
|
||||
|
||||
|
||||
def handle_stars(text: str) -> str:
|
||||
@@ -395,7 +363,7 @@ def handle_stars(text: str) -> str:
|
||||
return "\n\n".join(output)
|
||||
|
||||
|
||||
def generate_atom_feed(articles: list[dict[Any, Any]], file_name: str) -> str: # noqa: PLR0914, PLR0915
|
||||
def generate_atom_feed(articles: list[dict[Any, Any]], file_name: str) -> str: # noqa: C901, PLR0914, PLR0915
|
||||
"""Generate an Atom feed from a list of articles.
|
||||
|
||||
Args:
|
||||
@@ -413,11 +381,7 @@ def generate_atom_feed(articles: list[dict[Any, Any]], file_name: str) -> str:
|
||||
if articles:
|
||||
latest_entry = articles[0].get("createTime", "")
|
||||
if latest_entry:
|
||||
latest_entry = (
|
||||
datetime.strptime(str(latest_entry), "%Y-%m-%d %H:%M:%S")
|
||||
.replace(tzinfo=UTC)
|
||||
.isoformat()
|
||||
)
|
||||
latest_entry = datetime.strptime(str(latest_entry), "%Y-%m-%d %H:%M:%S").replace(tzinfo=UTC).isoformat()
|
||||
|
||||
for article in articles:
|
||||
article_id: str = str(article.get("articleId", ""))
|
||||
@@ -434,65 +398,72 @@ def generate_atom_feed(articles: list[dict[Any, Any]], file_name: str) -> str:
|
||||
if not article_content:
|
||||
article_content = article_title
|
||||
|
||||
article_content_converted = str(MARKDOWN_CONVERTER.convert(article_content).strip()) # type: ignore # noqa: PGH003
|
||||
converter: MarkdownConverter = MarkdownConverter(heading_style="ATX", strip=["pre", "code"])
|
||||
article_content_converted = str(converter.convert(article_content).strip())
|
||||
|
||||
if not article_content_converted:
|
||||
msg: str = f"Article content is empty for article ID: {article_id}"
|
||||
logger.warning(msg)
|
||||
article_content_converted = "No content available"
|
||||
|
||||
# Combine non-breaking space replacements in one pass
|
||||
content = NON_BREAKING_SPACE_PATTERN.sub(" ", article_content_converted)
|
||||
# Remove non-breaking spaces
|
||||
xa0_removed: str = NON_BREAKING_SPACE_PATTERN.sub(" ", article_content_converted) # Replace non-breaking spaces with regular spaces
|
||||
|
||||
# Remove empty code blocks
|
||||
content = EMPTY_CODE_BLOCK_PATTERN.sub("", content)
|
||||
# Replace non-breaking spaces with regular spaces
|
||||
non_breaking_space_removed: str = xa0_removed.replace(" ", " ") # noqa: RUF001
|
||||
|
||||
# Remove code blocks that has only spaces and newlines inside them
|
||||
empty_code_block_removed: str = EMPTY_CODE_BLOCK_PATTERN.sub("", non_breaking_space_removed)
|
||||
|
||||
# [How to Update] should be # How to Update
|
||||
content = SQUARE_BRACKETS_PATTERN.sub(r"# \1", content)
|
||||
square_brackets_converted: str = SQUARE_BRACKETS_PATTERN.sub(r"# \1", empty_code_block_removed)
|
||||
|
||||
content = handle_stars(content)
|
||||
stars_converted: str = handle_stars(square_brackets_converted)
|
||||
|
||||
# If `● Word` is in the content, replace it `## Word` instead
|
||||
content = BALL_PATTERN.sub(r"\n\n## \1\n\n", content)
|
||||
# If `● Word` is in the content, replace it `## Word` instead with regex
|
||||
ball_converted: str = BALL_PATTERN.sub(r"\n\n## \1\n\n", stars_converted)
|
||||
|
||||
# If `※ Word` is in the content, replace it `* word * ` instead
|
||||
content = REFERENCE_MARK_PATTERN.sub(r"\n\n*\1*\n\n", content)
|
||||
# If `※ Word` is in the content, replace it `* word * ` instead with regex
|
||||
reference_mark_converted: str = REFERENCE_MARK_PATTERN.sub(r"\n\n*\1*\n\n", ball_converted)
|
||||
|
||||
# Replace circled Unicode numbers with plain numbered text (using precompiled patterns)
|
||||
for number, pattern in CIRCLED_NUMBERS.values():
|
||||
content = pattern.sub(rf"\n\n{number}. \1\n\n", content)
|
||||
|
||||
content = ESCAPED_STAR_PATTERN.sub(r"* \1", content)
|
||||
|
||||
markdown_formatted: str = mdformat.text( # type: ignore # noqa: PGH003
|
||||
content,
|
||||
options={
|
||||
"number": True, # Allow 1., 2., 3. numbering
|
||||
},
|
||||
# Replace circled Unicode numbers (①-⑩) with plain numbered text (e.g., "1. ", "2. ", ..., "10. ")
|
||||
for pattern, number in CIRCLED_NUMBER_PATTERNS.values():
|
||||
reference_mark_converted = pattern.sub(
|
||||
rf"\n\n{number}. \1\n\n",
|
||||
reference_mark_converted,
|
||||
)
|
||||
|
||||
space_before_star_added: str = ESCAPED_STAR_PATTERN.sub(
|
||||
r"* \1",
|
||||
reference_mark_converted,
|
||||
)
|
||||
|
||||
# Format Markdown safely. mdformat doesn't support a "number" option here,
|
||||
# and unknown options can raise at runtime. We avoid passing invalid options
|
||||
# and fall back to the raw text if formatting fails for any reason.
|
||||
try:
|
||||
formatter: Any = mdformat # Help the type checker by treating mdformat as Any here
|
||||
markdown_formatted: str = str(formatter.text(space_before_star_added))
|
||||
except Exception:
|
||||
logger.exception("mdformat failed; using unformatted markdown text")
|
||||
markdown_formatted = space_before_star_added
|
||||
|
||||
links_fixed: str = format_discord_links(markdown_formatted)
|
||||
article_escaped: Markup = escape(links_fixed)
|
||||
|
||||
article_url: str = (
|
||||
f"https://wutheringwaves.kurogames.com/en/main/news/detail/{article_id}"
|
||||
)
|
||||
article_url: str = f"https://wutheringwaves.kurogames.com/en/main/news/detail/{article_id}"
|
||||
article_create_time: str = article.get("createTime", "")
|
||||
published: str = ""
|
||||
updated: str = latest_entry
|
||||
|
||||
if article_create_time:
|
||||
timestamp: datetime = datetime.strptime(
|
||||
str(article_create_time), "%Y-%m-%d %H:%M:%S"
|
||||
).replace(tzinfo=UTC)
|
||||
timestamp: datetime = datetime.strptime(str(article_create_time), "%Y-%m-%d %H:%M:%S").replace(tzinfo=UTC)
|
||||
iso_time: str = timestamp.isoformat()
|
||||
published = f"<published>{iso_time}</published>"
|
||||
updated = iso_time
|
||||
|
||||
article_category: str = article.get("articleTypeName", "Wuthering Waves")
|
||||
category: str = (
|
||||
f'<category term="{escape(article_category)}"/>' if article_category else ""
|
||||
)
|
||||
category: str = f'<category term="{escape(article_category)}"/>' if article_category else ""
|
||||
|
||||
html: str = markdown.markdown(
|
||||
text=article_escaped,
|
||||
@@ -526,7 +497,7 @@ def generate_atom_feed(articles: list[dict[Any, Any]], file_name: str) -> str:
|
||||
html_file: Path = html_dir / f"{article_id}.html"
|
||||
if not html_file.is_file():
|
||||
with html_file.open("w", encoding="utf-8") as f:
|
||||
f.write(html)
|
||||
f.write(str(BeautifulSoup(html, "html.parser").prettify()))
|
||||
logger.info("Saved HTML for article %s to %s", article_id, html_file)
|
||||
|
||||
# Set the file timestamp
|
||||
@@ -553,35 +524,12 @@ def generate_atom_feed(articles: list[dict[Any, Any]], file_name: str) -> str:
|
||||
</author>
|
||||
{"".join(atom_entries)}
|
||||
</feed>
|
||||
""" # noqa: E501
|
||||
"""
|
||||
|
||||
return atom_feed
|
||||
|
||||
|
||||
def load_all_articles(output_dir: Path) -> list[dict[Any, Any]]:
|
||||
"""Load all article JSON files from the output directory.
|
||||
|
||||
Args:
|
||||
output_dir (Path): The directory containing article JSON files.
|
||||
|
||||
Returns:
|
||||
list[dict[Any, Any]]: List of article data dictionaries.
|
||||
"""
|
||||
articles: list[dict[Any, Any]] = []
|
||||
for file in output_dir.glob("*.json"):
|
||||
if file.stem == "ArticleMenu":
|
||||
continue
|
||||
with file.open("r", encoding="utf-8") as f:
|
||||
try:
|
||||
article_data: dict[Any, Any] = json.load(f)
|
||||
articles.append(article_data)
|
||||
except json.JSONDecodeError:
|
||||
logger.exception("Error decoding JSON from %s", file)
|
||||
continue
|
||||
return articles
|
||||
|
||||
|
||||
def create_atom_feeds(articles: list[dict[Any, Any]], output_dir: Path) -> None:
|
||||
def create_atom_feeds(output_dir: Path) -> None:
|
||||
"""Create Atom feeds for the articles.
|
||||
|
||||
Current feeds are:
|
||||
@@ -589,16 +537,28 @@ def create_atom_feeds(articles: list[dict[Any, Any]], output_dir: Path) -> None:
|
||||
- All articles
|
||||
|
||||
Args:
|
||||
articles (list[dict[Any, Any]]): List of article data.
|
||||
output_dir (Path): The directory to save the RSS feed files.
|
||||
|
||||
"""
|
||||
if not articles:
|
||||
logger.error("Can't create Atom feeds, no articles provided")
|
||||
menu_data: list[dict[Any, Any]] = []
|
||||
# Load data from all the articles
|
||||
for file in output_dir.glob("*.json"):
|
||||
if file.stem == "ArticleMenu":
|
||||
continue
|
||||
with file.open("r", encoding="utf-8") as f:
|
||||
try:
|
||||
article_data: dict[Any, Any] = json.load(f)
|
||||
menu_data.append(article_data)
|
||||
except json.JSONDecodeError:
|
||||
logger.exception("Error decoding JSON from %s", file)
|
||||
continue
|
||||
|
||||
if not menu_data:
|
||||
logger.error("Can't create Atom feeds, no articles found in %s", output_dir)
|
||||
return
|
||||
|
||||
articles_sorted: list[dict[Any, Any]] = sorted(
|
||||
articles,
|
||||
menu_data,
|
||||
key=lambda x: get_file_timestamp(x.get("createTime", "")),
|
||||
reverse=True,
|
||||
)
|
||||
@@ -614,9 +574,7 @@ def create_atom_feeds(articles: list[dict[Any, Any]], output_dir: Path) -> None:
|
||||
article_create_time: str = article.get("createTime", "")
|
||||
logger.info("\tArticle ID: %s, Date: %s", article_id, article_create_time)
|
||||
|
||||
atom_feed: str = generate_atom_feed(
|
||||
articles=latest_articles, file_name=atom_feed_path.name
|
||||
)
|
||||
atom_feed: str = generate_atom_feed(articles=latest_articles, file_name=atom_feed_path.name)
|
||||
with atom_feed_path.open("w", encoding="utf-8") as f:
|
||||
f.write(atom_feed)
|
||||
logger.info(
|
||||
@@ -627,9 +585,7 @@ def create_atom_feeds(articles: list[dict[Any, Any]], output_dir: Path) -> None:
|
||||
|
||||
# Create the Atom feed for all articles
|
||||
atom_feed_path_all: Path = Path("articles_all.xml")
|
||||
atom_feed_all_articles: str = generate_atom_feed(
|
||||
articles=articles_sorted, file_name=atom_feed_path_all.name
|
||||
)
|
||||
atom_feed_all_articles: str = generate_atom_feed(articles=articles_sorted, file_name=atom_feed_path_all.name)
|
||||
with atom_feed_path_all.open("w", encoding="utf-8") as f:
|
||||
f.write(atom_feed_all_articles)
|
||||
logger.info("Created Atom feed for all articles: %s", atom_feed_path_all)
|
||||
@@ -692,13 +648,11 @@ async def main() -> Literal[1, 0]:
|
||||
"""
|
||||
# Setup
|
||||
current_time = int(time.time() * 1000) # Current time in milliseconds
|
||||
base_url = (
|
||||
"https://hw-media-cdn-mingchao.kurogame.com/akiwebsite/website2.0/json/G152/en"
|
||||
)
|
||||
base_url = "https://hw-media-cdn-mingchao.kurogame.com/akiwebsite/website2.0/json/G152/en"
|
||||
article_menu_url: str = f"{base_url}/ArticleMenu.json?t={current_time}"
|
||||
article_base_url: str = f"{base_url}/article/"
|
||||
output_dir = Path("articles")
|
||||
output_dir.mkdir(exist_ok=True)
|
||||
await anyio.Path(output_dir).mkdir(exist_ok=True)
|
||||
|
||||
logger.info("Fetching article menu from %s", article_menu_url)
|
||||
|
||||
@@ -716,29 +670,19 @@ async def main() -> Literal[1, 0]:
|
||||
|
||||
# Extract article IDs
|
||||
logger.info("Extracting article IDs...")
|
||||
article_ids: list[str] = [
|
||||
str(item["articleId"]) for item in menu_data if item.get("articleId")
|
||||
]
|
||||
article_ids: list[str] = [str(item["articleId"]) for item in menu_data if item.get("articleId")]
|
||||
|
||||
if not article_ids:
|
||||
logger.warning(
|
||||
"No article IDs found. Please check the JSON structure of ArticleMenu.json."
|
||||
)
|
||||
logger.warning("No article IDs found. Please check the JSON structure of ArticleMenu.json.")
|
||||
logger.warning("Full menu response for debugging:")
|
||||
logger.warning(json.dumps(menu_data, indent=2))
|
||||
return 1
|
||||
|
||||
# Get list of already downloaded article IDs
|
||||
existing_files: list[str] = [
|
||||
file.stem
|
||||
for file in output_dir.glob("*.json")
|
||||
if file.stem != "ArticleMenu"
|
||||
]
|
||||
existing_files: list[str] = [file.stem async for file in anyio.Path(output_dir).glob("*.json") if file.stem != "ArticleMenu"]
|
||||
|
||||
# Filter out already downloaded articles
|
||||
new_article_ids: list[str] = [
|
||||
article_id for article_id in article_ids if article_id not in existing_files
|
||||
]
|
||||
# Filter out already downloaded articles
|
||||
new_article_ids: list[str] = [article_id for article_id in article_ids if article_id not in existing_files]
|
||||
|
||||
if new_article_ids:
|
||||
logger.info("Found %s new articles to download", len(new_article_ids))
|
||||
@@ -746,18 +690,14 @@ async def main() -> Literal[1, 0]:
|
||||
# Download each new article
|
||||
download_tasks: list[Coroutine[Any, Any, dict[Any, Any] | None]] = []
|
||||
for article_id in new_article_ids:
|
||||
article_url: str = (
|
||||
f"{article_base_url}{article_id}.json?t={current_time}"
|
||||
)
|
||||
article_url: str = f"{article_base_url}{article_id}.json?t={current_time}"
|
||||
output_file: Path = output_dir / f"{article_id}.json"
|
||||
|
||||
logger.info("Downloading article %s from %s", article_id, article_url)
|
||||
download_tasks.append(fetch_json(article_url, client))
|
||||
|
||||
# Wait for all downloads to complete
|
||||
results: list[dict[Any, Any] | BaseException | None] = await asyncio.gather(
|
||||
*download_tasks, return_exceptions=True
|
||||
)
|
||||
results: list[dict[Any, Any] | BaseException | None] = await asyncio.gather(*download_tasks, return_exceptions=True)
|
||||
|
||||
# Process the downloaded articles
|
||||
for i, result in enumerate(results):
|
||||
@@ -769,27 +709,18 @@ async def main() -> Literal[1, 0]:
|
||||
continue
|
||||
|
||||
if not result:
|
||||
logger.warning(
|
||||
"Downloaded article %s is empty or invalid", article_id
|
||||
)
|
||||
logger.warning("Downloaded article %s is empty or invalid", article_id)
|
||||
continue
|
||||
|
||||
# Save the article JSON
|
||||
if isinstance(result, dict) and await save_prettified_json(
|
||||
result, output_file
|
||||
):
|
||||
logger.info(
|
||||
"Successfully downloaded and prettified %s", output_file
|
||||
)
|
||||
if isinstance(result, dict) and await save_prettified_json(result, output_file):
|
||||
logger.info("Successfully downloaded and prettified %s", output_file)
|
||||
else:
|
||||
logger.info("No new articles to download")
|
||||
|
||||
# Load all articles once for efficient processing
|
||||
all_articles = load_all_articles(output_dir)
|
||||
|
||||
add_data_to_articles(menu_data, output_dir)
|
||||
add_articles_to_readme(menu_data)
|
||||
create_atom_feeds(all_articles, output_dir)
|
||||
create_atom_feeds(output_dir)
|
||||
batch_process_timestamps(menu_data, output_dir)
|
||||
|
||||
logger.info("Script finished. Articles are in the '%s' directory.", output_dir)
|
||||
|
||||
237
uv.lock
generated
237
uv.lock
generated
@@ -1,237 +0,0 @@
|
||||
version = 1
|
||||
revision = 2
|
||||
requires-python = ">=3.13"
|
||||
|
||||
[[package]]
|
||||
name = "aiofiles"
|
||||
version = "24.1.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/0b/03/a88171e277e8caa88a4c77808c20ebb04ba74cc4681bf1e9416c862de237/aiofiles-24.1.0.tar.gz", hash = "sha256:22a075c9e5a3810f0c2e48f3008c94d68c65d763b9b03857924c99e57355166c", size = 30247, upload-time = "2024-06-24T11:02:03.584Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/a5/45/30bb92d442636f570cb5651bc661f52b610e2eec3f891a5dc3a4c3667db0/aiofiles-24.1.0-py3-none-any.whl", hash = "sha256:b4ec55f4195e3eb5d7abd1bf7e061763e864dd4954231fb8539a0ef8bb8260e5", size = 15896, upload-time = "2024-06-24T11:02:01.529Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anyio"
|
||||
version = "4.9.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "idna" },
|
||||
{ name = "sniffio" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/95/7d/4c1bd541d4dffa1b52bd83fb8527089e097a106fc90b467a7313b105f840/anyio-4.9.0.tar.gz", hash = "sha256:673c0c244e15788651a4ff38710fea9675823028a6f08a5eda409e0c9840a028", size = 190949, upload-time = "2025-03-17T00:02:54.77Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/a1/ee/48ca1a7c89ffec8b6a0c5d02b89c305671d5ffd8d3c94acf8b8c408575bb/anyio-4.9.0-py3-none-any.whl", hash = "sha256:9f76d541cad6e36af7beb62e978876f3b41e3e04f2c1fbf0884604c0a9c4d93c", size = 100916, upload-time = "2025-03-17T00:02:52.713Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "beautifulsoup4"
|
||||
version = "4.13.4"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "soupsieve" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d8/e4/0c4c39e18fd76d6a628d4dd8da40543d136ce2d1752bd6eeeab0791f4d6b/beautifulsoup4-4.13.4.tar.gz", hash = "sha256:dbb3c4e1ceae6aefebdaf2423247260cd062430a410e38c66f2baa50a8437195", size = 621067, upload-time = "2025-04-15T17:05:13.836Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/50/cd/30110dc0ffcf3b131156077b90e9f60ed75711223f306da4db08eff8403b/beautifulsoup4-4.13.4-py3-none-any.whl", hash = "sha256:9bbbb14bfde9d79f38b8cd5f8c7c85f4b8f2523190ebed90e950a8dea4cb1c4b", size = 187285, upload-time = "2025-04-15T17:05:12.221Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "certifi"
|
||||
version = "2025.7.9"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/de/8a/c729b6b60c66a38f590c4e774decc4b2ec7b0576be8f1aa984a53ffa812a/certifi-2025.7.9.tar.gz", hash = "sha256:c1d2ec05395148ee10cf672ffc28cd37ea0ab0d99f9cc74c43e588cbd111b079", size = 160386, upload-time = "2025-07-09T02:13:58.874Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/66/f3/80a3f974c8b535d394ff960a11ac20368e06b736da395b551a49ce950cce/certifi-2025.7.9-py3-none-any.whl", hash = "sha256:d842783a14f8fdd646895ac26f719a061408834473cfc10203f6a575beb15d39", size = 159230, upload-time = "2025-07-09T02:13:57.007Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "h11"
|
||||
version = "0.16.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "httpcore"
|
||||
version = "1.0.9"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "certifi" },
|
||||
{ name = "h11" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "httpx"
|
||||
version = "0.28.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "anyio" },
|
||||
{ name = "certifi" },
|
||||
{ name = "httpcore" },
|
||||
{ name = "idna" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "3.10"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490, upload-time = "2024-09-15T18:07:39.745Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "markdown"
|
||||
version = "3.8.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d7/c2/4ab49206c17f75cb08d6311171f2d65798988db4360c4d1485bd0eedd67c/markdown-3.8.2.tar.gz", hash = "sha256:247b9a70dd12e27f67431ce62523e675b866d254f900c4fe75ce3dda62237c45", size = 362071, upload-time = "2025-06-19T17:12:44.483Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/96/2b/34cc11786bc00d0f04d0f5fdc3a2b1ae0b6239eef72d3d345805f9ad92a1/markdown-3.8.2-py3-none-any.whl", hash = "sha256:5c83764dbd4e00bdd94d85a19b8d55ccca20fe35b2e678a1422b380324dd5f24", size = 106827, upload-time = "2025-06-19T17:12:42.994Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "markdown-it-py"
|
||||
version = "3.0.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "mdurl" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596, upload-time = "2023-06-03T06:41:14.443Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528, upload-time = "2023-06-03T06:41:11.019Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "markdownify"
|
||||
version = "1.1.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "beautifulsoup4" },
|
||||
{ name = "six" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/2f/78/c48fed23c7aebc2c16049062e72de1da3220c274de59d28c942acdc9ffb2/markdownify-1.1.0.tar.gz", hash = "sha256:449c0bbbf1401c5112379619524f33b63490a8fa479456d41de9dc9e37560ebd", size = 17127, upload-time = "2025-03-05T11:54:40.574Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/64/11/b751af7ad41b254a802cf52f7bc1fca7cabe2388132f2ce60a1a6b9b9622/markdownify-1.1.0-py3-none-any.whl", hash = "sha256:32a5a08e9af02c8a6528942224c91b933b4bd2c7d078f9012943776fc313eeef", size = 13901, upload-time = "2025-03-05T11:54:39.454Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "markupsafe"
|
||||
version = "3.0.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/b2/97/5d42485e71dfc078108a86d6de8fa46db44a1a9295e89c5d6d4a06e23a62/markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0", size = 20537, upload-time = "2024-10-18T15:21:54.129Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/83/0e/67eb10a7ecc77a0c2bbe2b0235765b98d164d81600746914bebada795e97/MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd", size = 14274, upload-time = "2024-10-18T15:21:24.577Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2b/6d/9409f3684d3335375d04e5f05744dfe7e9f120062c9857df4ab490a1031a/MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430", size = 12352, upload-time = "2024-10-18T15:21:25.382Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d2/f5/6eadfcd3885ea85fe2a7c128315cc1bb7241e1987443d78c8fe712d03091/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094", size = 24122, upload-time = "2024-10-18T15:21:26.199Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0c/91/96cf928db8236f1bfab6ce15ad070dfdd02ed88261c2afafd4b43575e9e9/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396", size = 23085, upload-time = "2024-10-18T15:21:27.029Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c2/cf/c9d56af24d56ea04daae7ac0940232d31d5a8354f2b457c6d856b2057d69/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79", size = 22978, upload-time = "2024-10-18T15:21:27.846Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2a/9f/8619835cd6a711d6272d62abb78c033bda638fdc54c4e7f4272cf1c0962b/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a", size = 24208, upload-time = "2024-10-18T15:21:28.744Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f9/bf/176950a1792b2cd2102b8ffeb5133e1ed984547b75db47c25a67d3359f77/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca", size = 23357, upload-time = "2024-10-18T15:21:29.545Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ce/4f/9a02c1d335caabe5c4efb90e1b6e8ee944aa245c1aaaab8e8a618987d816/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c", size = 23344, upload-time = "2024-10-18T15:21:30.366Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ee/55/c271b57db36f748f0e04a759ace9f8f759ccf22b4960c270c78a394f58be/MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1", size = 15101, upload-time = "2024-10-18T15:21:31.207Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/29/88/07df22d2dd4df40aba9f3e402e6dc1b8ee86297dddbad4872bd5e7b0094f/MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f", size = 15603, upload-time = "2024-10-18T15:21:32.032Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/62/6a/8b89d24db2d32d433dffcd6a8779159da109842434f1dd2f6e71f32f738c/MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c", size = 14510, upload-time = "2024-10-18T15:21:33.625Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7a/06/a10f955f70a2e5a9bf78d11a161029d278eeacbd35ef806c3fd17b13060d/MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb", size = 12486, upload-time = "2024-10-18T15:21:34.611Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/34/cf/65d4a571869a1a9078198ca28f39fba5fbb910f952f9dbc5220afff9f5e6/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c", size = 25480, upload-time = "2024-10-18T15:21:35.398Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0c/e3/90e9651924c430b885468b56b3d597cabf6d72be4b24a0acd1fa0e12af67/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d", size = 23914, upload-time = "2024-10-18T15:21:36.231Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/66/8c/6c7cf61f95d63bb866db39085150df1f2a5bd3335298f14a66b48e92659c/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe", size = 23796, upload-time = "2024-10-18T15:21:37.073Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bb/35/cbe9238ec3f47ac9a7c8b3df7a808e7cb50fe149dc7039f5f454b3fba218/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5", size = 25473, upload-time = "2024-10-18T15:21:37.932Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e6/32/7621a4382488aa283cc05e8984a9c219abad3bca087be9ec77e89939ded9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a", size = 24114, upload-time = "2024-10-18T15:21:39.799Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0d/80/0985960e4b89922cb5a0bac0ed39c5b96cbc1a536a99f30e8c220a996ed9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9", size = 24098, upload-time = "2024-10-18T15:21:40.813Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/82/78/fedb03c7d5380df2427038ec8d973587e90561b2d90cd472ce9254cf348b/MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6", size = 15208, upload-time = "2024-10-18T15:21:41.814Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4f/65/6079a46068dfceaeabb5dcad6d674f5f5c61a6fa5673746f42a9f4c233b3/MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f", size = 15739, upload-time = "2024-10-18T15:21:42.784Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mdformat"
|
||||
version = "0.7.22"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "markdown-it-py" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/fc/eb/b5cbf2484411af039a3d4aeb53a5160fae25dd8c84af6a4243bc2f3fedb3/mdformat-0.7.22.tar.gz", hash = "sha256:eef84fa8f233d3162734683c2a8a6222227a229b9206872e6139658d99acb1ea", size = 34610, upload-time = "2025-01-30T18:00:51.418Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/f2/6f/94a7344f6d634fe3563bea8b33bccedee37f2726f7807e9a58440dc91627/mdformat-0.7.22-py3-none-any.whl", hash = "sha256:61122637c9e1d9be1329054f3fa216559f0d1f722b7919b060a8c2a4ae1850e5", size = 34447, upload-time = "2025-01-30T18:00:48.708Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mdurl"
|
||||
version = "0.1.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "six"
|
||||
version = "1.17.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sniffio"
|
||||
version = "1.3.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "soupsieve"
|
||||
version = "2.7"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/3f/f4/4a80cd6ef364b2e8b65b15816a843c0980f7a5a2b4dc701fc574952aa19f/soupsieve-2.7.tar.gz", hash = "sha256:ad282f9b6926286d2ead4750552c8a6142bc4c783fd66b0293547c8fe6ae126a", size = 103418, upload-time = "2025-04-20T18:50:08.518Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/e7/9c/0e6afc12c269578be5c0c1c9f4b49a8d32770a080260c333ac04cc1c832d/soupsieve-2.7-py3-none-any.whl", hash = "sha256:6e60cc5c1ffaf1cebcc12e8188320b72071e922c2e897f737cadce79ad5d30c4", size = 36677, upload-time = "2025-04-20T18:50:07.196Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typing-extensions"
|
||||
version = "4.14.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/98/5a/da40306b885cc8c09109dc2e1abd358d5684b1425678151cdaed4731c822/typing_extensions-4.14.1.tar.gz", hash = "sha256:38b39f4aeeab64884ce9f74c94263ef78f3c22467c8724005483154c26648d36", size = 107673, upload-time = "2025-07-04T13:28:34.16Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/b5/00/d631e67a838026495268c2f6884f3711a15a9a2a96cd244fdaea53b823fb/typing_extensions-4.14.1-py3-none-any.whl", hash = "sha256:d1e1e3b58374dc93031d6eda2420a48ea44a36c2b4766a4fdeb3710755731d76", size = 43906, upload-time = "2025-07-04T13:28:32.743Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wutheringwaves"
|
||||
version = "0.1.0"
|
||||
source = { virtual = "." }
|
||||
dependencies = [
|
||||
{ name = "aiofiles" },
|
||||
{ name = "beautifulsoup4" },
|
||||
{ name = "httpx" },
|
||||
{ name = "markdown" },
|
||||
{ name = "markdownify" },
|
||||
{ name = "markupsafe" },
|
||||
{ name = "mdformat" },
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "aiofiles" },
|
||||
{ name = "beautifulsoup4" },
|
||||
{ name = "httpx" },
|
||||
{ name = "markdown", specifier = ">=3.8" },
|
||||
{ name = "markdownify" },
|
||||
{ name = "markupsafe" },
|
||||
{ name = "mdformat" },
|
||||
]
|
||||
Reference in New Issue
Block a user