switch to TOML config and export republisher feed manifests
This commit is contained in:
parent
98dcea4d7e
commit
897af2872c
17 changed files with 832 additions and 324 deletions
2
.envrc
Normal file
2
.envrc
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
use flake
|
||||
dotenv_if_exists
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -5,3 +5,6 @@ logs/
|
|||
dev/
|
||||
__pycache__
|
||||
NOTES
|
||||
.direnv
|
||||
*egg-info
|
||||
.env
|
||||
|
|
|
|||
38
README.md
Normal file
38
README.md
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
# pygea
|
||||
|
||||
Generate RSS feeds from the Pangea HTTP API using a TOML runtime config.
|
||||
|
||||
```shell
|
||||
nix develop
|
||||
uv sync --all-groups
|
||||
cp demo/pygea.toml pygea.toml
|
||||
$EDITOR pygea.toml
|
||||
uv run pygea --config pygea.toml
|
||||
```
|
||||
|
||||
Each `[[feeds]]` entry is explicit and stable:
|
||||
|
||||
```toml
|
||||
[[feeds]]
|
||||
name = "Info Martí "
|
||||
slug = "info-marti"
|
||||
only_newest = false
|
||||
```
|
||||
|
||||
`slug` is required and is used as the output subdirectory name. Generated feeds are
|
||||
written to `<output_directory>/<slug>/rss.xml`. A `manifest.toml` is also written to
|
||||
`<output_directory>/manifest.toml` using the same `[[feeds]]` structure that
|
||||
`republisher-redux` can import directly:
|
||||
|
||||
```toml
|
||||
[[feeds]]
|
||||
name = "Info Martí "
|
||||
slug = "info-marti"
|
||||
url = "file:///absolute/path/to/feed/info-marti/rss.xml"
|
||||
```
|
||||
|
||||
Omit `content_type` on a feed to use `default_content_type`.
|
||||
|
||||
`PYGEA_API_KEY` takes precedence over `runtime.api_key` in the TOML config.
|
||||
|
||||
See [`demo/README.md`](/home/abel/src/gitlab.com/guardianproject-ops/pygea/demo/README.md) for a self-contained example.
|
||||
20
demo/README.md
Normal file
20
demo/README.md
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
# Demo
|
||||
|
||||
This directory shows the TOML runtime-config layout for `pygea`.
|
||||
|
||||
## Local Run
|
||||
|
||||
From the repo root:
|
||||
|
||||
```shell
|
||||
uv run pygea --config demo/pygea.toml
|
||||
```
|
||||
|
||||
Because `output_directory` in [`demo/pygea.toml`](/home/abel/src/gitlab.com/guardianproject-ops/pygea/demo/pygea.toml) is relative, output is written under `demo/feed/`.
|
||||
|
||||
## Files
|
||||
|
||||
- `pygea.toml`: example runtime config with domain, feed definitions, runtime options, and output paths
|
||||
|
||||
After a successful run, `demo/feed/manifest.toml` will contain `[[feeds]]` entries
|
||||
with absolute `file://` URLs pointing at the generated `rss.xml` files.
|
||||
46
demo/pygea.toml
Normal file
46
demo/pygea.toml
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
domain = "www.martinoticias.com"
|
||||
default_content_type = "articles"
|
||||
|
||||
[[feeds]]
|
||||
name = "Titulares"
|
||||
slug = "titulares"
|
||||
only_newest = true
|
||||
|
||||
[[feeds]]
|
||||
name = "Cuba"
|
||||
slug = "cuba"
|
||||
only_newest = true
|
||||
|
||||
[[feeds]]
|
||||
name = "América Latina"
|
||||
slug = "america-latina"
|
||||
only_newest = true
|
||||
|
||||
[[feeds]]
|
||||
name = "Info Martí "
|
||||
slug = "info-marti"
|
||||
only_newest = false
|
||||
|
||||
[[feeds]]
|
||||
name = "Noticiero Martí Noticias"
|
||||
slug = "noticiero-marti-noticias"
|
||||
only_newest = true
|
||||
|
||||
[runtime]
|
||||
# api_key = "set-me-or-use-PYGEA_API_KEY"
|
||||
max_articles = 10
|
||||
oldest_article = 3
|
||||
authors_p = true
|
||||
no_media_p = false
|
||||
content_inc_p = true
|
||||
content_format = "MOBILE_3"
|
||||
verbose_p = true
|
||||
|
||||
[results]
|
||||
output_to_file_p = true
|
||||
output_file_name = "rss.xml"
|
||||
output_directory = "./feed"
|
||||
|
||||
[logging]
|
||||
log_file = "./logs/pangea.log"
|
||||
default_log_level = "WARNING"
|
||||
10
flake.nix
10
flake.nix
|
|
@ -76,6 +76,7 @@
|
|||
ps.beautifulsoup4
|
||||
ps.feedgen
|
||||
ps."python-dateutil"
|
||||
ps.pytest
|
||||
]);
|
||||
|
||||
smokeCheck = pkgs.runCommand "pygea-smoke" { nativeBuildInputs = [ smokePython ]; } ''
|
||||
|
|
@ -91,6 +92,14 @@
|
|||
touch "$out/passed"
|
||||
'';
|
||||
|
||||
pytestCheck = pkgs.runCommand "pygea-pytest" { nativeBuildInputs = [ smokePython ]; } ''
|
||||
export PYTHONPATH="${./.}:$PYTHONPATH"
|
||||
cd ${./.}
|
||||
pytest
|
||||
mkdir -p "$out"
|
||||
touch "$out/passed"
|
||||
'';
|
||||
|
||||
deadnixCheck = pkgs.runCommand "pygea-deadnix" { nativeBuildInputs = [ pkgs.deadnix ]; } ''
|
||||
cd ${./.}
|
||||
deadnix --fail .
|
||||
|
|
@ -111,6 +120,7 @@
|
|||
package-default = exportedPackage;
|
||||
treefmt = treefmtConfig.build.check ./.;
|
||||
smoke = smokeCheck;
|
||||
pytest = pytestCheck;
|
||||
deadnix = deadnixCheck;
|
||||
statix = statixCheck;
|
||||
}
|
||||
|
|
|
|||
217
pygea/config.py
Normal file
217
pygea/config.py
Normal file
|
|
@ -0,0 +1,217 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
import tomllib
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import TypedDict
|
||||
|
||||
SLUG_PATTERN = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._-]*$")
|
||||
|
||||
|
||||
class FeedDefinition(TypedDict):
|
||||
name: str
|
||||
slug: str
|
||||
only_newest: bool
|
||||
content_type: str | None
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class RuntimeConfig:
|
||||
api_key: str | None
|
||||
max_articles: int
|
||||
oldest_article: int
|
||||
authors_p: bool
|
||||
no_media_p: bool
|
||||
content_inc_p: bool
|
||||
content_format: str
|
||||
verbose_p: bool
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ResultsConfig:
|
||||
output_to_file_p: bool
|
||||
output_file_name: str
|
||||
output_directory: Path
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class LoggingConfig:
|
||||
log_file: Path
|
||||
default_log_level: str
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class PygeaConfig:
|
||||
config_path: Path
|
||||
domain: str
|
||||
default_content_type: str
|
||||
feeds: tuple[FeedDefinition, ...]
|
||||
runtime: RuntimeConfig
|
||||
results: ResultsConfig
|
||||
logging: LoggingConfig
|
||||
|
||||
|
||||
def _require_string(value: object, field_name: str) -> str:
|
||||
if not isinstance(value, str) or not value:
|
||||
raise ValueError(f"Config field {field_name!r} must be a non-empty string")
|
||||
return value
|
||||
|
||||
|
||||
def _require_bool(value: object, field_name: str) -> bool:
|
||||
if not isinstance(value, bool):
|
||||
raise ValueError(f"Config field {field_name!r} must be a boolean")
|
||||
return value
|
||||
|
||||
|
||||
def _require_int(value: object, field_name: str, *, minimum: int = 0) -> int:
|
||||
if not isinstance(value, int) or isinstance(value, bool) or value < minimum:
|
||||
raise ValueError(f"Config field {field_name!r} must be an integer >= {minimum}")
|
||||
return value
|
||||
|
||||
|
||||
def _resolve_path(config_path: Path, value: str) -> Path:
|
||||
path = Path(value).expanduser()
|
||||
if not path.is_absolute():
|
||||
path = (config_path.parent / path).resolve()
|
||||
return path
|
||||
|
||||
|
||||
def _parse_feeds(raw_config: dict[str, object]) -> tuple[FeedDefinition, ...]:
|
||||
raw_feeds = raw_config.get("feeds")
|
||||
if not isinstance(raw_feeds, list) or not raw_feeds:
|
||||
raise ValueError("Config must include at least one [[feeds]] entry")
|
||||
|
||||
feeds: list[FeedDefinition] = []
|
||||
feed_slugs: set[str] = set()
|
||||
for index, raw_feed in enumerate(raw_feeds, start=1):
|
||||
if not isinstance(raw_feed, dict):
|
||||
raise ValueError("Each [[feeds]] entry must be a table")
|
||||
name = _require_string(raw_feed.get("name"), f"feeds[{index}].name")
|
||||
slug = _require_string(raw_feed.get("slug"), f"feeds[{index}].slug")
|
||||
if SLUG_PATTERN.fullmatch(slug) is None:
|
||||
raise ValueError(f"Feed slug {slug!r} must match {SLUG_PATTERN.pattern!r}")
|
||||
if slug in feed_slugs:
|
||||
raise ValueError(f"Feed slug {slug!r} is duplicated")
|
||||
feed_slugs.add(slug)
|
||||
|
||||
only_newest = _require_bool(
|
||||
raw_feed.get("only_newest", True),
|
||||
f"feeds[{index}].only_newest",
|
||||
)
|
||||
content_type = raw_feed.get("content_type")
|
||||
if content_type is not None and (
|
||||
not isinstance(content_type, str) or not content_type
|
||||
):
|
||||
raise ValueError(
|
||||
f"Config field 'feeds[{index}].content_type' must be a non-empty string or null"
|
||||
)
|
||||
|
||||
feeds.append(
|
||||
{
|
||||
"name": name,
|
||||
"slug": slug,
|
||||
"only_newest": only_newest,
|
||||
"content_type": content_type,
|
||||
}
|
||||
)
|
||||
|
||||
return tuple(feeds)
|
||||
|
||||
|
||||
def load_config(path: str | Path) -> PygeaConfig:
|
||||
config_path = Path(path).expanduser().resolve()
|
||||
with config_path.open("rb") as config_file:
|
||||
raw_config = tomllib.load(config_file)
|
||||
|
||||
domain = _require_string(raw_config.get("domain"), "domain")
|
||||
default_content_type = _require_string(
|
||||
raw_config.get("default_content_type", "articles"),
|
||||
"default_content_type",
|
||||
)
|
||||
feeds = _parse_feeds(raw_config)
|
||||
|
||||
raw_runtime = raw_config.get("runtime", {})
|
||||
if not isinstance(raw_runtime, dict):
|
||||
raise ValueError("Config field 'runtime' must be a table")
|
||||
api_key = raw_runtime.get("api_key")
|
||||
if api_key is not None and (not isinstance(api_key, str) or not api_key):
|
||||
raise ValueError(
|
||||
"Config field 'runtime.api_key' must be a non-empty string or omitted"
|
||||
)
|
||||
runtime = RuntimeConfig(
|
||||
api_key=api_key,
|
||||
max_articles=_require_int(
|
||||
raw_runtime.get("max_articles", 10), "runtime.max_articles", minimum=1
|
||||
),
|
||||
oldest_article=_require_int(
|
||||
raw_runtime.get("oldest_article", 3),
|
||||
"runtime.oldest_article",
|
||||
minimum=0,
|
||||
),
|
||||
authors_p=_require_bool(
|
||||
raw_runtime.get("authors_p", True), "runtime.authors_p"
|
||||
),
|
||||
no_media_p=_require_bool(
|
||||
raw_runtime.get("no_media_p", False), "runtime.no_media_p"
|
||||
),
|
||||
content_inc_p=_require_bool(
|
||||
raw_runtime.get("content_inc_p", True),
|
||||
"runtime.content_inc_p",
|
||||
),
|
||||
content_format=_require_string(
|
||||
raw_runtime.get("content_format", "MOBILE_3"),
|
||||
"runtime.content_format",
|
||||
),
|
||||
verbose_p=_require_bool(
|
||||
raw_runtime.get("verbose_p", True), "runtime.verbose_p"
|
||||
),
|
||||
)
|
||||
|
||||
raw_results = raw_config.get("results", {})
|
||||
if not isinstance(raw_results, dict):
|
||||
raise ValueError("Config field 'results' must be a table")
|
||||
output_file_name = _require_string(
|
||||
raw_results.get("output_file_name", "rss.xml"),
|
||||
"results.output_file_name",
|
||||
)
|
||||
results = ResultsConfig(
|
||||
output_to_file_p=_require_bool(
|
||||
raw_results.get("output_to_file_p", True),
|
||||
"results.output_to_file_p",
|
||||
),
|
||||
output_file_name=output_file_name,
|
||||
output_directory=_resolve_path(
|
||||
config_path,
|
||||
_require_string(
|
||||
raw_results.get("output_directory", "./feed"),
|
||||
"results.output_directory",
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
raw_logging = raw_config.get("logging", {})
|
||||
if not isinstance(raw_logging, dict):
|
||||
raise ValueError("Config field 'logging' must be a table")
|
||||
logging = LoggingConfig(
|
||||
log_file=_resolve_path(
|
||||
config_path,
|
||||
_require_string(
|
||||
raw_logging.get("log_file", "./logs/pangea.log"), "logging.log_file"
|
||||
),
|
||||
),
|
||||
default_log_level=_require_string(
|
||||
raw_logging.get("default_log_level", "WARNING"),
|
||||
"logging.default_log_level",
|
||||
),
|
||||
)
|
||||
|
||||
return PygeaConfig(
|
||||
config_path=config_path,
|
||||
domain=domain,
|
||||
default_content_type=default_content_type,
|
||||
feeds=feeds,
|
||||
runtime=runtime,
|
||||
results=results,
|
||||
logging=logging,
|
||||
)
|
||||
159
pygea/main.py
159
pygea/main.py
|
|
@ -1,96 +1,107 @@
|
|||
"""Pygea main entry point"""
|
||||
"""Pygea main entry point."""
|
||||
|
||||
import hashlib
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
from pygea import utilities
|
||||
from pygea.pangeafeed import PangeaFeed
|
||||
from pygea.config import FeedDefinition, PygeaConfig, load_config
|
||||
from pygea.pexception import PangeaServiceException
|
||||
|
||||
OUTPUT_TO_FILE = utilities.get_configuration_variable("results", "output_to_file_p")
|
||||
OUTPUT_FILE_NAME = utilities.get_configuration_variable("results", "output_file_name")
|
||||
OUTPUT_DIRECTORY = utilities.get_configuration_variable("results", "output_directory")
|
||||
|
||||
def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(description="Generate RSS feeds from Pangea")
|
||||
parser.add_argument(
|
||||
"-c",
|
||||
"--config",
|
||||
default="pygea.toml",
|
||||
help="Path to runtime config TOML file",
|
||||
)
|
||||
return parser.parse_args(argv)
|
||||
|
||||
|
||||
def write_manifest(categories):
|
||||
"""Write the category manifest beside the generated feed output."""
|
||||
if OUTPUT_TO_FILE is not True:
|
||||
def _toml_string(value: str) -> str:
|
||||
return json.dumps(value, ensure_ascii=False)
|
||||
|
||||
|
||||
def render_manifest(feeds: list[dict[str, str]]) -> str:
|
||||
lines: list[str] = []
|
||||
for feed in feeds:
|
||||
lines.extend(
|
||||
[
|
||||
"[[feeds]]",
|
||||
f"name = {_toml_string(feed['name'])}",
|
||||
f"slug = {_toml_string(feed['slug'])}",
|
||||
f"url = {_toml_string(feed['url'])}",
|
||||
"",
|
||||
]
|
||||
)
|
||||
return "\n".join(lines).rstrip() + "\n"
|
||||
|
||||
|
||||
def write_manifest(config: PygeaConfig, manifest_feeds: list[dict[str, str]]) -> None:
|
||||
"""Write the feed manifest beside the generated feed output."""
|
||||
if config.results.output_to_file_p is not True:
|
||||
return
|
||||
|
||||
output_directory = os.path.normpath(OUTPUT_DIRECTORY)
|
||||
if not os.path.exists(output_directory):
|
||||
os.makedirs(output_directory)
|
||||
|
||||
manifest_path = os.path.join(output_directory, "manifest.json")
|
||||
with open(manifest_path, "w", encoding="utf-8") as mfile:
|
||||
json.dump({"categories": categories}, mfile, indent=2, ensure_ascii=False)
|
||||
mfile.write("\n")
|
||||
config.results.output_directory.mkdir(parents=True, exist_ok=True)
|
||||
manifest_path = config.results.output_directory / "manifest.toml"
|
||||
manifest_path.write_text(render_manifest(manifest_feeds), encoding="utf-8")
|
||||
|
||||
|
||||
def main():
|
||||
# Feeds are generated for a single, specified, domain
|
||||
domain = "www.martinoticias.com"
|
||||
def feed_class():
|
||||
from pygea.pangeafeed import PangeaFeed
|
||||
|
||||
args = {
|
||||
# tuple values:
|
||||
# [0] category name or a string representing a content query
|
||||
# [1] only the newest content desired (as configured in pygea.ini)?
|
||||
# [2] special content_type for this category only (from the approved list of types)
|
||||
"categories": [
|
||||
("Titulares", True, None),
|
||||
("Cuba", True, None),
|
||||
("América Latina", True, None),
|
||||
(
|
||||
"Info Martí ",
|
||||
False,
|
||||
None,
|
||||
), # YES! this category name has a space character at the end!
|
||||
("Noticiero Martí Noticias", True, None),
|
||||
],
|
||||
"default_content_type": "articles",
|
||||
}
|
||||
return PangeaFeed
|
||||
|
||||
# TWO OPTIONS from the args defined above:
|
||||
# 1. Generate a single feed from the defined categories
|
||||
# try:
|
||||
# pf = PangeaFeed(domain, args)
|
||||
# pf.acquire_content()
|
||||
# pf.generate_feed()
|
||||
# pf.disgorge()
|
||||
# except PangeaServiceException as error:
|
||||
# print(error)
|
||||
|
||||
# 2. Generate different feeds for each defined category
|
||||
def main(argv: list[str] | None = None) -> int:
|
||||
args = parse_args(argv)
|
||||
try:
|
||||
manifest_categories = []
|
||||
for cat_tuple in args["categories"]:
|
||||
# form new args for each category/query
|
||||
newargs = {"categories": [cat_tuple], "default_content_type": "articles"}
|
||||
pf = PangeaFeed(domain, newargs)
|
||||
config = load_config(args.config)
|
||||
except FileNotFoundError:
|
||||
print(
|
||||
"Config file not found: {}".format(Path(args.config).expanduser()),
|
||||
file=sys.stderr,
|
||||
)
|
||||
print(
|
||||
"Use --config PATH or create pygea.toml in the project root",
|
||||
file=sys.stderr,
|
||||
)
|
||||
return 2
|
||||
|
||||
try:
|
||||
manifest_feeds: list[dict[str, str]] = []
|
||||
pangea_feed_class = feed_class()
|
||||
for feed in config.feeds:
|
||||
pf = pangea_feed_class(config, [feed])
|
||||
pf.acquire_content()
|
||||
pf.generate_feed()
|
||||
# put each feed into a different sub-directory
|
||||
feed_subdir = hashlib.md5(cat_tuple[0].encode("utf-8")).hexdigest()[:7]
|
||||
pf.disgorge(feed_subdir)
|
||||
manifest_categories.append(
|
||||
{
|
||||
"name": cat_tuple[0],
|
||||
"short-hash": feed_subdir,
|
||||
"local-path": os.path.join(feed_subdir, OUTPUT_FILE_NAME).replace(
|
||||
os.sep, "/"
|
||||
),
|
||||
}
|
||||
)
|
||||
print(
|
||||
"feed for {} output to sub-directory {}".format(
|
||||
cat_tuple[0], feed_subdir
|
||||
output_path = pf.disgorge(feed["slug"])
|
||||
if output_path is not None:
|
||||
manifest_feeds.append(_manifest_entry(feed, output_path))
|
||||
print(
|
||||
"feed for {} output to sub-directory {}".format(
|
||||
feed["name"], feed["slug"]
|
||||
)
|
||||
)
|
||||
)
|
||||
write_manifest(manifest_categories)
|
||||
write_manifest(config, manifest_feeds)
|
||||
except PangeaServiceException as error:
|
||||
print(error)
|
||||
print(error, file=sys.stderr)
|
||||
return 1
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
def _manifest_entry(feed: FeedDefinition, output_path: Path) -> dict[str, str]:
|
||||
return {
|
||||
"name": feed["name"],
|
||||
"slug": feed["slug"],
|
||||
"url": output_path.resolve().as_uri(),
|
||||
}
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
sys.exit(main())
|
||||
|
|
|
|||
|
|
@ -7,86 +7,85 @@ categories or content filters and an optional supplied content-type.
|
|||
- * -
|
||||
|
||||
"""
|
||||
import os
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
from feedgen.feed import FeedGenerator
|
||||
|
||||
from pygea import pangeaservice, pexception, utilities
|
||||
|
||||
VERBOSE = utilities.get_configuration_variable("runtime", "verbose_p")
|
||||
OUTPUT_TO_FILE = utilities.get_configuration_variable("results", "output_to_file_p")
|
||||
OUTPUT_FILE_NAME = utilities.get_configuration_variable("results", "output_file_name")
|
||||
OUTPUT_DIRECTORY = utilities.get_configuration_variable("results", "output_directory")
|
||||
from pygea.config import FeedDefinition, PygeaConfig
|
||||
|
||||
|
||||
class PangeaFeed:
|
||||
|
||||
_domain = None
|
||||
_categories = None
|
||||
_content_type = "articles" # default
|
||||
|
||||
def __init__(self, domain, kw_args):
|
||||
def __init__(self, config: PygeaConfig, feeds: list[FeedDefinition]):
|
||||
try:
|
||||
self._ps = pangeaservice.PangeaService(domain)
|
||||
self._ps = pangeaservice.PangeaService(config)
|
||||
except pexception.PangeaServiceException as error:
|
||||
raise error
|
||||
|
||||
self._domain = domain
|
||||
if kw_args.get("categories"):
|
||||
self._categories = kw_args["categories"]
|
||||
else:
|
||||
self._config = config
|
||||
self._domain = config.domain
|
||||
if not feeds:
|
||||
raise pexception.PangeaServiceException(
|
||||
"ERROR: At least one category or content-query is required"
|
||||
)
|
||||
self._feeds = tuple(feeds)
|
||||
|
||||
if kw_args.get("default_content_type"):
|
||||
if kw_args["default_content_type"] not in self._ps.content_types():
|
||||
if config.default_content_type not in self._ps.content_types():
|
||||
raise pexception.PangeaServiceException(
|
||||
"{} is not a valid content type".format(config.default_content_type)
|
||||
)
|
||||
self._content_type = config.default_content_type
|
||||
|
||||
for feed in self._feeds:
|
||||
if (
|
||||
feed["content_type"] is not None
|
||||
and feed["content_type"] not in self._ps.content_types()
|
||||
):
|
||||
raise pexception.PangeaServiceException(
|
||||
"{} is not a valid content type".format(kw_args["content_type"])
|
||||
"{} is not a valid content type".format(feed["content_type"])
|
||||
)
|
||||
self._content_type = kw_args["default_content_type"]
|
||||
|
||||
def acquire_content(self):
|
||||
self._full_article_list = []
|
||||
|
||||
for cat, old, type in self._categories:
|
||||
for feed in self._feeds:
|
||||
opt_args = {}
|
||||
# special type for this category?
|
||||
if type is None:
|
||||
type = self._content_type
|
||||
# wants old stuff (not configured date limit)?
|
||||
if old is not None:
|
||||
content_type = feed["content_type"] or self._content_type
|
||||
if not feed["only_newest"]:
|
||||
opt_args["daycount"] = 365 # oldest date = one year
|
||||
opt_args["filter_date"] = False
|
||||
|
||||
ci = self._ps.category_info(cat)
|
||||
if ci is not None:
|
||||
# cat is pre-defined category
|
||||
opt_args["zoneid"] = ci["id"]
|
||||
jbody = self._ps.get_content(type, opt_args)
|
||||
category_info = self._ps.category_info(feed["name"])
|
||||
if category_info is not None:
|
||||
opt_args["zoneid"] = category_info["id"]
|
||||
jbody = self._ps.get_content(content_type, opt_args)
|
||||
else:
|
||||
# cat as actually a free-form query string to be used no article content
|
||||
jbody = self._ps.query_content(cat, opt_args)
|
||||
jbody = self._ps.query_content(feed["name"], opt_args)
|
||||
|
||||
if len(jbody) == 0:
|
||||
if VERBOSE:
|
||||
if self._config.runtime.verbose_p:
|
||||
print(
|
||||
"no articles available for {} [command: {}] [category/query: '{}'])".format(
|
||||
self._domain, self._content_type, cat
|
||||
self._domain, content_type, feed["name"]
|
||||
)
|
||||
)
|
||||
continue
|
||||
|
||||
if VERBOSE:
|
||||
if self._config.runtime.verbose_p:
|
||||
print(
|
||||
"{} articles added from category/query '{}'".format(
|
||||
str(len(jbody)), cat
|
||||
str(len(jbody)), feed["name"]
|
||||
)
|
||||
)
|
||||
|
||||
for art in jbody:
|
||||
self._full_article_list.append(art)
|
||||
self._full_article_list.extend(jbody)
|
||||
|
||||
def generate_feed(self):
|
||||
#
|
||||
|
|
@ -144,7 +143,7 @@ class PangeaFeed:
|
|||
article_deets = self._ps.get_article_detail(article["id"])
|
||||
rss_article = self._ps.rss_article_from_pangea_article(article_deets)
|
||||
except pexception.PangeaServiceException as error:
|
||||
if VERBOSE:
|
||||
if self._config.runtime.verbose_p:
|
||||
print(error)
|
||||
print(
|
||||
"article with id [{}] may no longer exist in Pangea".format(
|
||||
|
|
@ -178,7 +177,7 @@ class PangeaFeed:
|
|||
if not media_extension_loaded:
|
||||
fg.load_extension("media")
|
||||
media_extension_loaded = True
|
||||
if VERBOSE:
|
||||
if self._config.runtime.verbose_p:
|
||||
print("media extension loaded")
|
||||
|
||||
mc_md = rss_article["media_content"]
|
||||
|
|
@ -192,27 +191,24 @@ class PangeaFeed:
|
|||
else:
|
||||
fe.media.content(url=mc_md["url"])
|
||||
|
||||
def disgorge(self, subdirectory=None):
|
||||
def disgorge(self, slug: str | None = None) -> Path | None:
|
||||
#
|
||||
# Output the RSS feed as appropriate
|
||||
#
|
||||
if OUTPUT_TO_FILE is True:
|
||||
if self._config.results.output_to_file_p is True:
|
||||
try:
|
||||
if subdirectory is not None:
|
||||
if not os.path.exists(OUTPUT_DIRECTORY + "/" + subdirectory):
|
||||
os.makedirs(OUTPUT_DIRECTORY + "/" + subdirectory)
|
||||
ofile = (
|
||||
OUTPUT_DIRECTORY + "/" + subdirectory + "/" + OUTPUT_FILE_NAME
|
||||
)
|
||||
else:
|
||||
if not os.path.exists(OUTPUT_DIRECTORY):
|
||||
os.makedirs(OUTPUT_DIRECTORY)
|
||||
ofile = OUTPUT_DIRECTORY + "/" + OUTPUT_FILE_NAME
|
||||
self._fg.rss_file(ofile, extensions=True, pretty=True)
|
||||
except OSError as fe:
|
||||
print("for {} file error: ".format(ofile, str(fe)))
|
||||
output_directory = self._config.results.output_directory
|
||||
if slug is not None:
|
||||
output_directory = output_directory / slug
|
||||
output_directory.mkdir(parents=True, exist_ok=True)
|
||||
output_path = output_directory / self._config.results.output_file_name
|
||||
self._fg.rss_file(str(output_path), extensions=True, pretty=True)
|
||||
except OSError as file_error:
|
||||
print("for {} file error: {}".format(output_path, file_error))
|
||||
sys.exit(1)
|
||||
if VERBOSE:
|
||||
print("output written to {}".format(ofile))
|
||||
else:
|
||||
print(self._fg.rss_str(extensions=True, pretty=True))
|
||||
if self._config.runtime.verbose_p:
|
||||
print("output written to {}".format(output_path))
|
||||
return output_path.resolve()
|
||||
|
||||
print(self._fg.rss_str(extensions=True, pretty=True))
|
||||
return None
|
||||
|
|
|
|||
|
|
@ -24,12 +24,12 @@ import requests
|
|||
from dateutil.parser import *
|
||||
|
||||
from pygea import pexception, plogger, utilities
|
||||
from pygea.config import PygeaConfig
|
||||
|
||||
|
||||
class PangeaService:
|
||||
"""Interface to the Pangea API"""
|
||||
|
||||
_configuration_file_name = "pygea.ini"
|
||||
_api_path = "/api2/"
|
||||
_api_key = None
|
||||
|
||||
|
|
@ -108,38 +108,30 @@ class PangeaService:
|
|||
"JSON": 128, # Generates json structured content
|
||||
}
|
||||
|
||||
def __init__(self, domain, key=None, verbose=False):
|
||||
self._logger = plogger.PangeaServiceLogger()
|
||||
def __init__(self, config: PygeaConfig, key=None, verbose=False):
|
||||
self._logger = plogger.PangeaServiceLogger(config.logging)
|
||||
|
||||
#
|
||||
# resolve API key: PYGEA_API_KEY env var takes precedence over pygea.ini
|
||||
# resolve API key: PYGEA_API_KEY env var takes precedence over the TOML config
|
||||
#
|
||||
self._api_key = utilities.get_api_key()
|
||||
self._api_key = utilities.get_api_key(config)
|
||||
if not self._api_key:
|
||||
raise pexception.PangeaServiceException(
|
||||
"ERROR: No API key found. Set PYGEA_API_KEY env var or add api_key to [runtime] in pygea.ini"
|
||||
"ERROR: No API key found. Set PYGEA_API_KEY or configure runtime.api_key in pygea.toml"
|
||||
)
|
||||
|
||||
#
|
||||
# preset from configuration file
|
||||
#
|
||||
self._max_articles = int(
|
||||
utilities.get_configuration_variable("runtime", "max_articles")
|
||||
)
|
||||
self._oldest_article = int(
|
||||
utilities.get_configuration_variable("runtime", "oldest_article")
|
||||
)
|
||||
self._content_format = utilities.get_configuration_variable(
|
||||
"runtime", "content_format"
|
||||
)
|
||||
self._authors_p = utilities.get_configuration_variable("runtime", "authors_p")
|
||||
self._no_media_p = utilities.get_configuration_variable("runtime", "no_media_p")
|
||||
self._content_inc_p = utilities.get_configuration_variable(
|
||||
"runtime", "content_inc_p"
|
||||
)
|
||||
self._verbose_p = utilities.get_configuration_variable("runtime", "verbose_p")
|
||||
self._max_articles = config.runtime.max_articles
|
||||
self._oldest_article = config.runtime.oldest_article
|
||||
self._content_format = config.runtime.content_format
|
||||
self._authors_p = config.runtime.authors_p
|
||||
self._no_media_p = config.runtime.no_media_p
|
||||
self._content_inc_p = config.runtime.content_inc_p
|
||||
self._verbose_p = config.runtime.verbose_p
|
||||
|
||||
self._domain = domain
|
||||
self._domain = config.domain
|
||||
|
||||
# config file overrides on invocation
|
||||
if key is not None:
|
||||
|
|
@ -513,12 +505,9 @@ class PangeaService:
|
|||
def _retrieve_content(self, command, args_kw=None):
|
||||
"""Minimalist content retriever"""
|
||||
url = self._build_url(command, args_kw)
|
||||
# print('request URL: ' + url)
|
||||
response = requests.get(url, timeout=20)
|
||||
if response.status_code != 200:
|
||||
msg = "received status code {} from {}".format(
|
||||
str(response.status_code), url
|
||||
)
|
||||
msg = "received status code {}".format(str(response.status_code))
|
||||
self._logger.error(msg)
|
||||
raise pexception.PangeaServiceException(msg)
|
||||
if command == "empty":
|
||||
|
|
@ -693,7 +682,4 @@ class PangeaService:
|
|||
|
||||
url += "&" + key + "=" + value
|
||||
|
||||
if self._verbose_p:
|
||||
print("URL for request: " + url)
|
||||
|
||||
return url
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ Logger for the Pangea API Service
|
|||
|
||||
import logging
|
||||
|
||||
from pygea import utilities
|
||||
from pygea.config import LoggingConfig
|
||||
|
||||
|
||||
class PangeaServiceLogger:
|
||||
|
|
@ -12,7 +12,6 @@ class PangeaServiceLogger:
|
|||
Mostly, so that someone can replace this with a production logger later.
|
||||
"""
|
||||
|
||||
_configuration_file_name = "pygea.ini"
|
||||
_levels = {
|
||||
"NOTSET": 0,
|
||||
"DEBUG": 10,
|
||||
|
|
@ -22,22 +21,25 @@ class PangeaServiceLogger:
|
|||
"CRITICAL": 50,
|
||||
}
|
||||
|
||||
def __init__(self):
|
||||
#
|
||||
# preset from configuration file
|
||||
#
|
||||
lf = utilities.get_configuration_variable("logging", "log_file")
|
||||
dl = utilities.get_configuration_variable("logging", "default_log_level")
|
||||
if (dl is None) | (dl not in self._levels):
|
||||
dl = "DEBUG"
|
||||
def __init__(self, config: LoggingConfig):
|
||||
log_level_name = config.default_log_level
|
||||
if log_level_name not in self._levels:
|
||||
log_level_name = "DEBUG"
|
||||
|
||||
config.log_file.parent.mkdir(parents=True, exist_ok=True)
|
||||
self._logger = logging.getLogger("PangeaLogger")
|
||||
self._logger.propagate = False
|
||||
logging.basicConfig(
|
||||
filename=lf,
|
||||
level=self._levels[dl],
|
||||
format="[%(asctime)s] %(levelname)s: %(message)s",
|
||||
self._logger.setLevel(self._levels[log_level_name])
|
||||
for handler in list(self._logger.handlers):
|
||||
self._logger.removeHandler(handler)
|
||||
handler.close()
|
||||
|
||||
handler = logging.FileHandler(config.log_file, encoding="utf-8")
|
||||
handler.setLevel(self._levels[log_level_name])
|
||||
handler.setFormatter(
|
||||
logging.Formatter("[%(asctime)s] %(levelname)s: %(message)s")
|
||||
)
|
||||
self._logger.addHandler(handler)
|
||||
|
||||
def debug(self, message):
|
||||
"""Debug message"""
|
||||
|
|
|
|||
|
|
@ -7,12 +7,13 @@ Utilities for the Pangea CMS Service API
|
|||
"""
|
||||
import hashlib
|
||||
import os
|
||||
from configparser import ConfigParser, NoOptionError, NoSectionError
|
||||
from urllib.parse import urlparse
|
||||
|
||||
import requests
|
||||
from bs4 import BeautifulSoup
|
||||
|
||||
from pygea.config import PygeaConfig
|
||||
|
||||
|
||||
def acquire(url):
|
||||
"""Simple wrapper over the request object."""
|
||||
|
|
@ -55,7 +56,7 @@ def get_webpage_metadata(page_url):
|
|||
# of the metadata we require.
|
||||
#
|
||||
html_content = acquire(page_url)
|
||||
if html_content == None:
|
||||
if html_content is None:
|
||||
return None
|
||||
|
||||
soup = BeautifulSoup(html_content, "html.parser")
|
||||
|
|
@ -103,43 +104,12 @@ def get_media_metadata(image_url):
|
|||
return meta
|
||||
|
||||
|
||||
def make_boolean(bool_str):
|
||||
"""Convert a boolean string to an actual Boolean."""
|
||||
in_str = bool_str.lower()
|
||||
if (in_str != "true") & (in_str != "false"):
|
||||
return True # following Python conventions
|
||||
|
||||
if in_str == "true":
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def get_api_key():
|
||||
"""Return the API key. PYGEA_API_KEY env var takes precedence over pygea.ini.
|
||||
Returns None if neither source provides a value."""
|
||||
def get_api_key(config: PygeaConfig):
|
||||
"""Return the API key, preferring the environment over the TOML config."""
|
||||
env_key = os.environ.get("PYGEA_API_KEY")
|
||||
if env_key:
|
||||
return env_key
|
||||
|
||||
config = ConfigParser()
|
||||
config.read("pygea.ini")
|
||||
try:
|
||||
return config.get("runtime", "api_key")
|
||||
except (NoSectionError, NoOptionError):
|
||||
return None
|
||||
|
||||
|
||||
def get_configuration_variable(section, vname):
|
||||
"""Retrieve values from the configuration file."""
|
||||
config = ConfigParser()
|
||||
config.read("pygea.ini")
|
||||
|
||||
value = config.get(section, vname)
|
||||
if (value == "True") | (value == "False"):
|
||||
value = make_boolean(value)
|
||||
|
||||
return value
|
||||
return config.runtime.api_key
|
||||
|
||||
|
||||
def is_domain_name(domain):
|
||||
|
|
|
|||
|
|
@ -2,7 +2,8 @@
|
|||
name = "pygea"
|
||||
version = "0.1.0"
|
||||
description = "Pangea RSS feed generator"
|
||||
requires-python = ">=3.10"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.13"
|
||||
dependencies = [
|
||||
"requests",
|
||||
"beautifulsoup4",
|
||||
|
|
@ -13,6 +14,16 @@ dependencies = [
|
|||
[project.scripts]
|
||||
pygea = "pygea.main:main"
|
||||
|
||||
[dependency-groups]
|
||||
dev = [
|
||||
"pytest>=8.3.5,<9.0.0",
|
||||
"black>=24.10.0,<25.0.0",
|
||||
"isort>=5.13.2,<6.0.0",
|
||||
]
|
||||
|
||||
[build-system]
|
||||
requires = ["hatchling"]
|
||||
build-backend = "hatchling.build"
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
testpaths = ["tests"]
|
||||
|
|
|
|||
6
tests/conftest.py
Normal file
6
tests/conftest.py
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
if str(ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(ROOT))
|
||||
73
tests/test_config.py
Normal file
73
tests/test_config.py
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from pygea.config import load_config
|
||||
|
||||
|
||||
def test_load_config_resolves_relative_paths_and_preserves_feed_fields(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
config_path = tmp_path / "configs" / "pygea.toml"
|
||||
config_path.parent.mkdir(parents=True)
|
||||
config_path.write_text(
|
||||
"""
|
||||
domain = "www.martinoticias.com"
|
||||
default_content_type = "articles"
|
||||
|
||||
[[feeds]]
|
||||
name = "Info Martí "
|
||||
slug = "info-marti"
|
||||
only_newest = false
|
||||
content_type = "articles"
|
||||
|
||||
[runtime]
|
||||
api_key = "demo-key"
|
||||
max_articles = 25
|
||||
oldest_article = 7
|
||||
verbose_p = false
|
||||
|
||||
[results]
|
||||
output_directory = "../feed-out"
|
||||
output_file_name = "rss.xml"
|
||||
|
||||
[logging]
|
||||
log_file = "../logs/pygea.log"
|
||||
default_log_level = "INFO"
|
||||
""".strip()
|
||||
+ "\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
config = load_config(config_path)
|
||||
|
||||
assert config.domain == "www.martinoticias.com"
|
||||
assert config.default_content_type == "articles"
|
||||
assert config.results.output_directory == (tmp_path / "feed-out").resolve()
|
||||
assert config.logging.log_file == (tmp_path / "logs" / "pygea.log").resolve()
|
||||
assert config.feeds == (
|
||||
{
|
||||
"name": "Info Martí ",
|
||||
"slug": "info-marti",
|
||||
"only_newest": False,
|
||||
"content_type": "articles",
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def test_load_config_rejects_invalid_slug(tmp_path: Path) -> None:
|
||||
config_path = tmp_path / "pygea.toml"
|
||||
config_path.write_text(
|
||||
"""
|
||||
domain = "www.martinoticias.com"
|
||||
|
||||
[[feeds]]
|
||||
name = "Titulares"
|
||||
slug = "bad slug"
|
||||
""".strip()
|
||||
+ "\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
with pytest.raises(ValueError, match="Feed slug"):
|
||||
load_config(config_path)
|
||||
84
tests/test_entrypoint.py
Normal file
84
tests/test_entrypoint.py
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
import tomllib
|
||||
from pathlib import Path
|
||||
|
||||
from pygea import main as main_module
|
||||
|
||||
|
||||
class StubPangeaFeed:
|
||||
def __init__(self, config, feeds):
|
||||
self.config = config
|
||||
self.feed = feeds[0]
|
||||
|
||||
def acquire_content(self) -> None:
|
||||
return None
|
||||
|
||||
def generate_feed(self) -> None:
|
||||
return None
|
||||
|
||||
def disgorge(self, slug: str):
|
||||
output_path = self.config.results.output_directory / slug / "rss.xml"
|
||||
output_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
output_path.write_text("<rss />\n", encoding="utf-8")
|
||||
return output_path
|
||||
|
||||
|
||||
def test_main_writes_manifest_toml_with_absolute_file_urls(
|
||||
tmp_path: Path, monkeypatch
|
||||
) -> None:
|
||||
config_path = tmp_path / "pygea.toml"
|
||||
config_path.write_text(
|
||||
"""
|
||||
domain = "www.martinoticias.com"
|
||||
|
||||
[[feeds]]
|
||||
name = "Info Martí "
|
||||
slug = "info-marti"
|
||||
only_newest = false
|
||||
|
||||
[[feeds]]
|
||||
name = "Titulares"
|
||||
slug = "titulares"
|
||||
only_newest = true
|
||||
content_type = "articles"
|
||||
|
||||
[runtime]
|
||||
api_key = "demo-key"
|
||||
verbose_p = false
|
||||
|
||||
[results]
|
||||
output_directory = "feed"
|
||||
output_file_name = "rss.xml"
|
||||
|
||||
[logging]
|
||||
log_file = "logs/pygea.log"
|
||||
default_log_level = "INFO"
|
||||
""".strip()
|
||||
+ "\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
monkeypatch.setattr(main_module, "feed_class", lambda: StubPangeaFeed)
|
||||
|
||||
exit_code = main_module.main(["--config", str(config_path)])
|
||||
|
||||
manifest_path = tmp_path / "feed" / "manifest.toml"
|
||||
assert exit_code == 0
|
||||
assert manifest_path.exists()
|
||||
|
||||
manifest = tomllib.loads(manifest_path.read_text(encoding="utf-8"))
|
||||
assert manifest == {
|
||||
"feeds": [
|
||||
{
|
||||
"name": "Info Martí ",
|
||||
"slug": "info-marti",
|
||||
"url": (tmp_path / "feed" / "info-marti" / "rss.xml")
|
||||
.resolve()
|
||||
.as_uri(),
|
||||
},
|
||||
{
|
||||
"name": "Titulares",
|
||||
"slug": "titulares",
|
||||
"url": (tmp_path / "feed" / "titulares" / "rss.xml").resolve().as_uri(),
|
||||
},
|
||||
]
|
||||
}
|
||||
255
uv.lock
generated
255
uv.lock
generated
|
|
@ -1,6 +1,6 @@
|
|||
version = 1
|
||||
revision = 3
|
||||
requires-python = ">=3.10"
|
||||
requires-python = ">=3.13"
|
||||
|
||||
[[package]]
|
||||
name = "beautifulsoup4"
|
||||
|
|
@ -15,6 +15,26 @@ wheels = [
|
|||
{ url = "https://files.pythonhosted.org/packages/1a/39/47f9197bdd44df24d67ac8893641e16f386c984a0619ef2ee4c51fbbc019/beautifulsoup4-4.14.3-py3-none-any.whl", hash = "sha256:0918bfe44902e6ad8d57732ba310582e98da931428d231a5ecb9e7c703a735bb", size = 107721, upload-time = "2025-11-30T15:08:24.087Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "black"
|
||||
version = "24.10.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "click" },
|
||||
{ name = "mypy-extensions" },
|
||||
{ name = "packaging" },
|
||||
{ name = "pathspec" },
|
||||
{ name = "platformdirs" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d8/0d/cc2fb42b8c50d80143221515dd7e4766995bd07c56c9a3ed30baf080b6dc/black-24.10.0.tar.gz", hash = "sha256:846ea64c97afe3bc677b761787993be4991810ecc7a4a937816dd6bddedc4875", size = 645813, upload-time = "2024-10-07T19:20:50.361Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d0/a0/a993f58d4ecfba035e61fca4e9f64a2ecae838fc9f33ab798c62173ed75c/black-24.10.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1cbacacb19e922a1d75ef2b6ccaefcd6e93a2c05ede32f06a21386a04cedb981", size = 1643986, upload-time = "2024-10-07T19:28:50.684Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/37/d5/602d0ef5dfcace3fb4f79c436762f130abd9ee8d950fa2abdbf8bbc555e0/black-24.10.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1f93102e0c5bb3907451063e08b9876dbeac810e7da5a8bfb7aeb5a9ef89066b", size = 1448085, upload-time = "2024-10-07T19:28:12.093Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/47/6d/a3a239e938960df1a662b93d6230d4f3e9b4a22982d060fc38c42f45a56b/black-24.10.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ddacb691cdcdf77b96f549cf9591701d8db36b2f19519373d60d31746068dbf2", size = 1760928, upload-time = "2024-10-07T19:24:15.233Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/dd/cf/af018e13b0eddfb434df4d9cd1b2b7892bab119f7a20123e93f6910982e8/black-24.10.0-cp313-cp313-win_amd64.whl", hash = "sha256:680359d932801c76d2e9c9068d05c6b107f2584b2a5b88831c83962eb9984c1b", size = 1436875, upload-time = "2024-10-07T19:24:42.762Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8d/a7/4b27c50537ebca8bec139b872861f9d2bf501c5ec51fcf897cb924d9e264/black-24.10.0-py3-none-any.whl", hash = "sha256:3bb2b7a1f7b685f85b11fed1ef10f8a9148bceb49853e47a294a3dd963c1dd7d", size = 206898, upload-time = "2024-10-07T19:20:48.317Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "certifi"
|
||||
version = "2026.1.4"
|
||||
|
|
@ -30,54 +50,6 @@ version = "3.4.4"
|
|||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/13/69/33ddede1939fdd074bce5434295f38fae7136463422fe4fd3e0e89b98062/charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a", size = 129418, upload-time = "2025-10-14T04:42:32.879Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/1f/b8/6d51fc1d52cbd52cd4ccedd5b5b2f0f6a11bbf6765c782298b0f3e808541/charset_normalizer-3.4.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e824f1492727fa856dd6eda4f7cee25f8518a12f3c4a56a74e8095695089cf6d", size = 209709, upload-time = "2025-10-14T04:40:11.385Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5c/af/1f9d7f7faafe2ddfb6f72a2e07a548a629c61ad510fe60f9630309908fef/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4bd5d4137d500351a30687c2d3971758aac9a19208fc110ccb9d7188fbe709e8", size = 148814, upload-time = "2025-10-14T04:40:13.135Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/79/3d/f2e3ac2bbc056ca0c204298ea4e3d9db9b4afe437812638759db2c976b5f/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:027f6de494925c0ab2a55eab46ae5129951638a49a34d87f4c3eda90f696b4ad", size = 144467, upload-time = "2025-10-14T04:40:14.728Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ec/85/1bf997003815e60d57de7bd972c57dc6950446a3e4ccac43bc3070721856/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f820802628d2694cb7e56db99213f930856014862f3fd943d290ea8438d07ca8", size = 162280, upload-time = "2025-10-14T04:40:16.14Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3e/8e/6aa1952f56b192f54921c436b87f2aaf7c7a7c3d0d1a765547d64fd83c13/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:798d75d81754988d2565bff1b97ba5a44411867c0cf32b77a7e8f8d84796b10d", size = 159454, upload-time = "2025-10-14T04:40:17.567Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/36/3b/60cbd1f8e93aa25d1c669c649b7a655b0b5fb4c571858910ea9332678558/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d1bb833febdff5c8927f922386db610b49db6e0d4f4ee29601d71e7c2694313", size = 153609, upload-time = "2025-10-14T04:40:19.08Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/64/91/6a13396948b8fd3c4b4fd5bc74d045f5637d78c9675585e8e9fbe5636554/charset_normalizer-3.4.4-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9cd98cdc06614a2f768d2b7286d66805f94c48cde050acdbbb7db2600ab3197e", size = 151849, upload-time = "2025-10-14T04:40:20.607Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b7/7a/59482e28b9981d105691e968c544cc0df3b7d6133152fb3dcdc8f135da7a/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:077fbb858e903c73f6c9db43374fd213b0b6a778106bc7032446a8e8b5b38b93", size = 151586, upload-time = "2025-10-14T04:40:21.719Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/92/59/f64ef6a1c4bdd2baf892b04cd78792ed8684fbc48d4c2afe467d96b4df57/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:244bfb999c71b35de57821b8ea746b24e863398194a4014e4c76adc2bbdfeff0", size = 145290, upload-time = "2025-10-14T04:40:23.069Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6b/63/3bf9f279ddfa641ffa1962b0db6a57a9c294361cc2f5fcac997049a00e9c/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:64b55f9dce520635f018f907ff1b0df1fdc31f2795a922fb49dd14fbcdf48c84", size = 163663, upload-time = "2025-10-14T04:40:24.17Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ed/09/c9e38fc8fa9e0849b172b581fd9803bdf6e694041127933934184e19f8c3/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:faa3a41b2b66b6e50f84ae4a68c64fcd0c44355741c6374813a800cd6695db9e", size = 151964, upload-time = "2025-10-14T04:40:25.368Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d2/d1/d28b747e512d0da79d8b6a1ac18b7ab2ecfd81b2944c4c710e166d8dd09c/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:6515f3182dbe4ea06ced2d9e8666d97b46ef4c75e326b79bb624110f122551db", size = 161064, upload-time = "2025-10-14T04:40:26.806Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bb/9a/31d62b611d901c3b9e5500c36aab0ff5eb442043fb3a1c254200d3d397d9/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cc00f04ed596e9dc0da42ed17ac5e596c6ccba999ba6bd92b0e0aef2f170f2d6", size = 155015, upload-time = "2025-10-14T04:40:28.284Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1f/f3/107e008fa2bff0c8b9319584174418e5e5285fef32f79d8ee6a430d0039c/charset_normalizer-3.4.4-cp310-cp310-win32.whl", hash = "sha256:f34be2938726fc13801220747472850852fe6b1ea75869a048d6f896838c896f", size = 99792, upload-time = "2025-10-14T04:40:29.613Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/eb/66/e396e8a408843337d7315bab30dbf106c38966f1819f123257f5520f8a96/charset_normalizer-3.4.4-cp310-cp310-win_amd64.whl", hash = "sha256:a61900df84c667873b292c3de315a786dd8dac506704dea57bc957bd31e22c7d", size = 107198, upload-time = "2025-10-14T04:40:30.644Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b5/58/01b4f815bf0312704c267f2ccb6e5d42bcc7752340cd487bc9f8c3710597/charset_normalizer-3.4.4-cp310-cp310-win_arm64.whl", hash = "sha256:cead0978fc57397645f12578bfd2d5ea9138ea0fac82b2f63f7f7c6877986a69", size = 100262, upload-time = "2025-10-14T04:40:32.108Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ed/27/c6491ff4954e58a10f69ad90aca8a1b6fe9c5d3c6f380907af3c37435b59/charset_normalizer-3.4.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6e1fcf0720908f200cd21aa4e6750a48ff6ce4afe7ff5a79a90d5ed8a08296f8", size = 206988, upload-time = "2025-10-14T04:40:33.79Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/94/59/2e87300fe67ab820b5428580a53cad894272dbb97f38a7a814a2a1ac1011/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f819d5fe9234f9f82d75bdfa9aef3a3d72c4d24a6e57aeaebba32a704553aa0", size = 147324, upload-time = "2025-10-14T04:40:34.961Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/07/fb/0cf61dc84b2b088391830f6274cb57c82e4da8bbc2efeac8c025edb88772/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a59cb51917aa591b1c4e6a43c132f0cdc3c76dbad6155df4e28ee626cc77a0a3", size = 142742, upload-time = "2025-10-14T04:40:36.105Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/62/8b/171935adf2312cd745d290ed93cf16cf0dfe320863ab7cbeeae1dcd6535f/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8ef3c867360f88ac904fd3f5e1f902f13307af9052646963ee08ff4f131adafc", size = 160863, upload-time = "2025-10-14T04:40:37.188Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/09/73/ad875b192bda14f2173bfc1bc9a55e009808484a4b256748d931b6948442/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d9e45d7faa48ee908174d8fe84854479ef838fc6a705c9315372eacbc2f02897", size = 157837, upload-time = "2025-10-14T04:40:38.435Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6d/fc/de9cce525b2c5b94b47c70a4b4fb19f871b24995c728e957ee68ab1671ea/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:840c25fb618a231545cbab0564a799f101b63b9901f2569faecd6b222ac72381", size = 151550, upload-time = "2025-10-14T04:40:40.053Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/55/c2/43edd615fdfba8c6f2dfbd459b25a6b3b551f24ea21981e23fb768503ce1/charset_normalizer-3.4.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ca5862d5b3928c4940729dacc329aa9102900382fea192fc5e52eb69d6093815", size = 149162, upload-time = "2025-10-14T04:40:41.163Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/03/86/bde4ad8b4d0e9429a4e82c1e8f5c659993a9a863ad62c7df05cf7b678d75/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9c7f57c3d666a53421049053eaacdd14bbd0a528e2186fcb2e672effd053bb0", size = 150019, upload-time = "2025-10-14T04:40:42.276Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1f/86/a151eb2af293a7e7bac3a739b81072585ce36ccfb4493039f49f1d3cae8c/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:277e970e750505ed74c832b4bf75dac7476262ee2a013f5574dd49075879e161", size = 143310, upload-time = "2025-10-14T04:40:43.439Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b5/fe/43dae6144a7e07b87478fdfc4dbe9efd5defb0e7ec29f5f58a55aeef7bf7/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:31fd66405eaf47bb62e8cd575dc621c56c668f27d46a61d975a249930dd5e2a4", size = 162022, upload-time = "2025-10-14T04:40:44.547Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/80/e6/7aab83774f5d2bca81f42ac58d04caf44f0cc2b65fc6db2b3b2e8a05f3b3/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:0d3d8f15c07f86e9ff82319b3d9ef6f4bf907608f53fe9d92b28ea9ae3d1fd89", size = 149383, upload-time = "2025-10-14T04:40:46.018Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4f/e8/b289173b4edae05c0dde07f69f8db476a0b511eac556dfe0d6bda3c43384/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:9f7fcd74d410a36883701fafa2482a6af2ff5ba96b9a620e9e0721e28ead5569", size = 159098, upload-time = "2025-10-14T04:40:47.081Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d8/df/fe699727754cae3f8478493c7f45f777b17c3ef0600e28abfec8619eb49c/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ebf3e58c7ec8a8bed6d66a75d7fb37b55e5015b03ceae72a8e7c74495551e224", size = 152991, upload-time = "2025-10-14T04:40:48.246Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1a/86/584869fe4ddb6ffa3bd9f491b87a01568797fb9bd8933f557dba9771beaf/charset_normalizer-3.4.4-cp311-cp311-win32.whl", hash = "sha256:eecbc200c7fd5ddb9a7f16c7decb07b566c29fa2161a16cf67b8d068bd21690a", size = 99456, upload-time = "2025-10-14T04:40:49.376Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/65/f6/62fdd5feb60530f50f7e38b4f6a1d5203f4d16ff4f9f0952962c044e919a/charset_normalizer-3.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:5ae497466c7901d54b639cf42d5b8c1b6a4fead55215500d2f486d34db48d016", size = 106978, upload-time = "2025-10-14T04:40:50.844Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7a/9d/0710916e6c82948b3be62d9d398cb4fcf4e97b56d6a6aeccd66c4b2f2bd5/charset_normalizer-3.4.4-cp311-cp311-win_arm64.whl", hash = "sha256:65e2befcd84bc6f37095f5961e68a6f077bf44946771354a28ad434c2cce0ae1", size = 99969, upload-time = "2025-10-14T04:40:52.272Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f3/85/1637cd4af66fa687396e757dec650f28025f2a2f5a5531a3208dc0ec43f2/charset_normalizer-3.4.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394", size = 208425, upload-time = "2025-10-14T04:40:53.353Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9d/6a/04130023fef2a0d9c62d0bae2649b69f7b7d8d24ea5536feef50551029df/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25", size = 148162, upload-time = "2025-10-14T04:40:54.558Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/78/29/62328d79aa60da22c9e0b9a66539feae06ca0f5a4171ac4f7dc285b83688/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef", size = 144558, upload-time = "2025-10-14T04:40:55.677Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/86/bb/b32194a4bf15b88403537c2e120b817c61cd4ecffa9b6876e941c3ee38fe/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f1e34719c6ed0b92f418c7c780480b26b5d9c50349e9a9af7d76bf757530350d", size = 161497, upload-time = "2025-10-14T04:40:57.217Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/19/89/a54c82b253d5b9b111dc74aca196ba5ccfcca8242d0fb64146d4d3183ff1/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2437418e20515acec67d86e12bf70056a33abdacb5cb1655042f6538d6b085a8", size = 159240, upload-time = "2025-10-14T04:40:58.358Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c0/10/d20b513afe03acc89ec33948320a5544d31f21b05368436d580dec4e234d/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11d694519d7f29d6cd09f6ac70028dba10f92f6cdd059096db198c283794ac86", size = 153471, upload-time = "2025-10-14T04:40:59.468Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/61/fa/fbf177b55bdd727010f9c0a3c49eefa1d10f960e5f09d1d887bf93c2e698/charset_normalizer-3.4.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ac1c4a689edcc530fc9d9aa11f5774b9e2f33f9a0c6a57864e90908f5208d30a", size = 150864, upload-time = "2025-10-14T04:41:00.623Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/05/12/9fbc6a4d39c0198adeebbde20b619790e9236557ca59fc40e0e3cebe6f40/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:21d142cc6c0ec30d2efee5068ca36c128a30b0f2c53c1c07bd78cb6bc1d3be5f", size = 150647, upload-time = "2025-10-14T04:41:01.754Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ad/1f/6a9a593d52e3e8c5d2b167daf8c6b968808efb57ef4c210acb907c365bc4/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5dbe56a36425d26d6cfb40ce79c314a2e4dd6211d51d6d2191c00bed34f354cc", size = 145110, upload-time = "2025-10-14T04:41:03.231Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/30/42/9a52c609e72471b0fc54386dc63c3781a387bb4fe61c20231a4ebcd58bdd/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5bfbb1b9acf3334612667b61bd3002196fe2a1eb4dd74d247e0f2a4d50ec9bbf", size = 162839, upload-time = "2025-10-14T04:41:04.715Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c4/5b/c0682bbf9f11597073052628ddd38344a3d673fda35a36773f7d19344b23/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:d055ec1e26e441f6187acf818b73564e6e6282709e9bcb5b63f5b23068356a15", size = 150667, upload-time = "2025-10-14T04:41:05.827Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e4/24/a41afeab6f990cf2daf6cb8c67419b63b48cf518e4f56022230840c9bfb2/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:af2d8c67d8e573d6de5bc30cdb27e9b95e49115cd9baad5ddbd1a6207aaa82a9", size = 160535, upload-time = "2025-10-14T04:41:06.938Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2a/e5/6a4ce77ed243c4a50a1fecca6aaaab419628c818a49434be428fe24c9957/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:780236ac706e66881f3b7f2f32dfe90507a09e67d1d454c762cf642e6e1586e0", size = 154816, upload-time = "2025-10-14T04:41:08.101Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a8/ef/89297262b8092b312d29cdb2517cb1237e51db8ecef2e9af5edbe7b683b1/charset_normalizer-3.4.4-cp312-cp312-win32.whl", hash = "sha256:5833d2c39d8896e4e19b689ffc198f08ea58116bee26dea51e362ecc7cd3ed26", size = 99694, upload-time = "2025-10-14T04:41:09.23Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3d/2d/1e5ed9dd3b3803994c155cd9aacb60c82c331bad84daf75bcb9c91b3295e/charset_normalizer-3.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:a79cfe37875f822425b89a82333404539ae63dbdddf97f84dcbc3d339aae9525", size = 107131, upload-time = "2025-10-14T04:41:10.467Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d0/d9/0ed4c7098a861482a7b6a95603edce4c0d9db2311af23da1fb2b75ec26fc/charset_normalizer-3.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:376bec83a63b8021bb5c8ea75e21c4ccb86e7e45ca4eb81146091b56599b80c3", size = 100390, upload-time = "2025-10-14T04:41:11.915Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/97/45/4b3a1239bbacd321068ea6e7ac28875b03ab8bc0aa0966452db17cd36714/charset_normalizer-3.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794", size = 208091, upload-time = "2025-10-14T04:41:13.346Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7d/62/73a6d7450829655a35bb88a88fca7d736f9882a27eacdca2c6d505b57e2e/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed", size = 147936, upload-time = "2025-10-14T04:41:14.461Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/89/c5/adb8c8b3d6625bef6d88b251bbb0d95f8205831b987631ab0c8bb5d937c2/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72", size = 144180, upload-time = "2025-10-14T04:41:15.588Z" },
|
||||
|
|
@ -113,6 +85,27 @@ wheels = [
|
|||
{ url = "https://files.pythonhosted.org/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", size = 53402, upload-time = "2025-10-14T04:42:31.76Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "click"
|
||||
version = "8.3.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/3d/fa/656b739db8587d7b5dfa22e22ed02566950fbfbcdc20311993483657a5c0/click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", size = 295065, upload-time = "2025-11-15T20:45:42.706Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274, upload-time = "2025-11-15T20:45:41.139Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "colorama"
|
||||
version = "0.4.6"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "feedgen"
|
||||
version = "1.0.0"
|
||||
|
|
@ -132,62 +125,30 @@ wheels = [
|
|||
{ url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "iniconfig"
|
||||
version = "2.3.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "isort"
|
||||
version = "5.13.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/87/f9/c1eb8635a24e87ade2efce21e3ce8cd6b8630bb685ddc9cdaca1349b2eb5/isort-5.13.2.tar.gz", hash = "sha256:48fdfcb9face5d58a4f6dde2e72a1fb8dcaf8ab26f95ab49fab84c2ddefb0109", size = 175303, upload-time = "2023-12-13T20:37:26.124Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/b3/8def84f539e7d2289a02f0524b944b15d7c75dab7628bedf1c4f0992029c/isort-5.13.2-py3-none-any.whl", hash = "sha256:8ca5e72a8d85860d5a3fa69b8745237f2939afe12dbf656afbcb47fe72d947a6", size = 92310, upload-time = "2023-12-13T20:37:23.244Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lxml"
|
||||
version = "6.0.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/aa/88/262177de60548e5a2bfc46ad28232c9e9cbde697bd94132aeb80364675cb/lxml-6.0.2.tar.gz", hash = "sha256:cd79f3367bd74b317dda655dc8fcfa304d9eb6e4fb06b7168c5cf27f96e0cd62", size = 4073426, upload-time = "2025-09-22T04:04:59.287Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/db/8a/f8192a08237ef2fb1b19733f709db88a4c43bc8ab8357f01cb41a27e7f6a/lxml-6.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e77dd455b9a16bbd2a5036a63ddbd479c19572af81b624e79ef422f929eef388", size = 8590589, upload-time = "2025-09-22T04:00:10.51Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/12/64/27bcd07ae17ff5e5536e8d88f4c7d581b48963817a13de11f3ac3329bfa2/lxml-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5d444858b9f07cefff6455b983aea9a67f7462ba1f6cbe4a21e8bf6791bf2153", size = 4629671, upload-time = "2025-09-22T04:00:15.411Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/02/5a/a7d53b3291c324e0b6e48f3c797be63836cc52156ddf8f33cd72aac78866/lxml-6.0.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f952dacaa552f3bb8834908dddd500ba7d508e6ea6eb8c52eb2d28f48ca06a31", size = 4999961, upload-time = "2025-09-22T04:00:17.619Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f5/55/d465e9b89df1761674d8672bb3e4ae2c47033b01ec243964b6e334c6743f/lxml-6.0.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:71695772df6acea9f3c0e59e44ba8ac50c4f125217e84aab21074a1a55e7e5c9", size = 5157087, upload-time = "2025-09-22T04:00:19.868Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/62/38/3073cd7e3e8dfc3ba3c3a139e33bee3a82de2bfb0925714351ad3d255c13/lxml-6.0.2-cp310-cp310-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:17f68764f35fd78d7c4cc4ef209a184c38b65440378013d24b8aecd327c3e0c8", size = 5067620, upload-time = "2025-09-22T04:00:21.877Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4a/d3/1e001588c5e2205637b08985597827d3827dbaaece16348c8822bfe61c29/lxml-6.0.2-cp310-cp310-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:058027e261afed589eddcfe530fcc6f3402d7fd7e89bfd0532df82ebc1563dba", size = 5406664, upload-time = "2025-09-22T04:00:23.714Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/20/cf/cab09478699b003857ed6ebfe95e9fb9fa3d3c25f1353b905c9b73cfb624/lxml-6.0.2-cp310-cp310-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8ffaeec5dfea5881d4c9d8913a32d10cfe3923495386106e4a24d45300ef79c", size = 5289397, upload-time = "2025-09-22T04:00:25.544Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a3/84/02a2d0c38ac9a8b9f9e5e1bbd3f24b3f426044ad618b552e9549ee91bd63/lxml-6.0.2-cp310-cp310-manylinux_2_31_armv7l.whl", hash = "sha256:f2e3b1a6bb38de0bc713edd4d612969dd250ca8b724be8d460001a387507021c", size = 4772178, upload-time = "2025-09-22T04:00:27.602Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/56/87/e1ceadcc031ec4aa605fe95476892d0b0ba3b7f8c7dcdf88fdeff59a9c86/lxml-6.0.2-cp310-cp310-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d6690ec5ec1cce0385cb20896b16be35247ac8c2046e493d03232f1c2414d321", size = 5358148, upload-time = "2025-09-22T04:00:29.323Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fe/13/5bb6cf42bb228353fd4ac5f162c6a84fd68a4d6f67c1031c8cf97e131fc6/lxml-6.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f2a50c3c1d11cad0ebebbac357a97b26aa79d2bcaf46f256551152aa85d3a4d1", size = 5112035, upload-time = "2025-09-22T04:00:31.061Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e4/e2/ea0498552102e59834e297c5c6dff8d8ded3db72ed5e8aad77871476f073/lxml-6.0.2-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:3efe1b21c7801ffa29a1112fab3b0f643628c30472d507f39544fd48e9549e34", size = 4799111, upload-time = "2025-09-22T04:00:33.11Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6a/9e/8de42b52a73abb8af86c66c969b3b4c2a96567b6ac74637c037d2e3baa60/lxml-6.0.2-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:59c45e125140b2c4b33920d21d83681940ca29f0b83f8629ea1a2196dc8cfe6a", size = 5351662, upload-time = "2025-09-22T04:00:35.237Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/28/a2/de776a573dfb15114509a37351937c367530865edb10a90189d0b4b9b70a/lxml-6.0.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:452b899faa64f1805943ec1c0c9ebeaece01a1af83e130b69cdefeda180bb42c", size = 5314973, upload-time = "2025-09-22T04:00:37.086Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/50/a0/3ae1b1f8964c271b5eec91db2043cf8c6c0bce101ebb2a633b51b044db6c/lxml-6.0.2-cp310-cp310-win32.whl", hash = "sha256:1e786a464c191ca43b133906c6903a7e4d56bef376b75d97ccbb8ec5cf1f0a4b", size = 3611953, upload-time = "2025-09-22T04:00:39.224Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/70/bd42491f0634aad41bdfc1e46f5cff98825fb6185688dc82baa35d509f1a/lxml-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:dacf3c64ef3f7440e3167aa4b49aa9e0fb99e0aa4f9ff03795640bf94531bcb0", size = 4032695, upload-time = "2025-09-22T04:00:41.402Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d2/d0/05c6a72299f54c2c561a6c6cbb2f512e047fca20ea97a05e57931f194ac4/lxml-6.0.2-cp310-cp310-win_arm64.whl", hash = "sha256:45f93e6f75123f88d7f0cfd90f2d05f441b808562bf0bc01070a00f53f5028b5", size = 3680051, upload-time = "2025-09-22T04:00:43.525Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/77/d5/becbe1e2569b474a23f0c672ead8a29ac50b2dc1d5b9de184831bda8d14c/lxml-6.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:13e35cbc684aadf05d8711a5d1b5857c92e5e580efa9a0d2be197199c8def607", size = 8634365, upload-time = "2025-09-22T04:00:45.672Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/28/66/1ced58f12e804644426b85d0bb8a4478ca77bc1761455da310505f1a3526/lxml-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3b1675e096e17c6fe9c0e8c81434f5736c0739ff9ac6123c87c2d452f48fc938", size = 4650793, upload-time = "2025-09-22T04:00:47.783Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/11/84/549098ffea39dfd167e3f174b4ce983d0eed61f9d8d25b7bf2a57c3247fc/lxml-6.0.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8ac6e5811ae2870953390452e3476694196f98d447573234592d30488147404d", size = 4944362, upload-time = "2025-09-22T04:00:49.845Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ac/bd/f207f16abf9749d2037453d56b643a7471d8fde855a231a12d1e095c4f01/lxml-6.0.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5aa0fc67ae19d7a64c3fe725dc9a1bb11f80e01f78289d05c6f62545affec438", size = 5083152, upload-time = "2025-09-22T04:00:51.709Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/15/ae/bd813e87d8941d52ad5b65071b1affb48da01c4ed3c9c99e40abb266fbff/lxml-6.0.2-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:de496365750cc472b4e7902a485d3f152ecf57bd3ba03ddd5578ed8ceb4c5964", size = 5023539, upload-time = "2025-09-22T04:00:53.593Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/02/cd/9bfef16bd1d874fbe0cb51afb00329540f30a3283beb9f0780adbb7eec03/lxml-6.0.2-cp311-cp311-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:200069a593c5e40b8f6fc0d84d86d970ba43138c3e68619ffa234bc9bb806a4d", size = 5344853, upload-time = "2025-09-22T04:00:55.524Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b8/89/ea8f91594bc5dbb879734d35a6f2b0ad50605d7fb419de2b63d4211765cc/lxml-6.0.2-cp311-cp311-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7d2de809c2ee3b888b59f995625385f74629707c9355e0ff856445cdcae682b7", size = 5225133, upload-time = "2025-09-22T04:00:57.269Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b9/37/9c735274f5dbec726b2db99b98a43950395ba3d4a1043083dba2ad814170/lxml-6.0.2-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:b2c3da8d93cf5db60e8858c17684c47d01fee6405e554fb55018dd85fc23b178", size = 4677944, upload-time = "2025-09-22T04:00:59.052Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/20/28/7dfe1ba3475d8bfca3878365075abe002e05d40dfaaeb7ec01b4c587d533/lxml-6.0.2-cp311-cp311-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:442de7530296ef5e188373a1ea5789a46ce90c4847e597856570439621d9c553", size = 5284535, upload-time = "2025-09-22T04:01:01.335Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e7/cf/5f14bc0de763498fc29510e3532bf2b4b3a1c1d5d0dff2e900c16ba021ef/lxml-6.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2593c77efde7bfea7f6389f1ab249b15ed4aa5bc5cb5131faa3b843c429fbedb", size = 5067343, upload-time = "2025-09-22T04:01:03.13Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1c/b0/bb8275ab5472f32b28cfbbcc6db7c9d092482d3439ca279d8d6fa02f7025/lxml-6.0.2-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:3e3cb08855967a20f553ff32d147e14329b3ae70ced6edc2f282b94afbc74b2a", size = 4725419, upload-time = "2025-09-22T04:01:05.013Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/25/4c/7c222753bc72edca3b99dbadba1b064209bc8ed4ad448af990e60dcce462/lxml-6.0.2-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:2ed6c667fcbb8c19c6791bbf40b7268ef8ddf5a96940ba9404b9f9a304832f6c", size = 5275008, upload-time = "2025-09-22T04:01:07.327Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6c/8c/478a0dc6b6ed661451379447cdbec77c05741a75736d97e5b2b729687828/lxml-6.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b8f18914faec94132e5b91e69d76a5c1d7b0c73e2489ea8929c4aaa10b76bbf7", size = 5248906, upload-time = "2025-09-22T04:01:09.452Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2d/d9/5be3a6ab2784cdf9accb0703b65e1b64fcdd9311c9f007630c7db0cfcce1/lxml-6.0.2-cp311-cp311-win32.whl", hash = "sha256:6605c604e6daa9e0d7f0a2137bdc47a2e93b59c60a65466353e37f8272f47c46", size = 3610357, upload-time = "2025-09-22T04:01:11.102Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e2/7d/ca6fb13349b473d5732fb0ee3eec8f6c80fc0688e76b7d79c1008481bf1f/lxml-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e5867f2651016a3afd8dd2c8238baa66f1e2802f44bc17e236f547ace6647078", size = 4036583, upload-time = "2025-09-22T04:01:12.766Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ab/a2/51363b5ecd3eab46563645f3a2c3836a2fc67d01a1b87c5017040f39f567/lxml-6.0.2-cp311-cp311-win_arm64.whl", hash = "sha256:4197fb2534ee05fd3e7afaab5d8bfd6c2e186f65ea7f9cd6a82809c887bd1285", size = 3680591, upload-time = "2025-09-22T04:01:14.874Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f3/c8/8ff2bc6b920c84355146cd1ab7d181bc543b89241cfb1ebee824a7c81457/lxml-6.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:a59f5448ba2ceccd06995c95ea59a7674a10de0810f2ce90c9006f3cbc044456", size = 8661887, upload-time = "2025-09-22T04:01:17.265Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/37/6f/9aae1008083bb501ef63284220ce81638332f9ccbfa53765b2b7502203cf/lxml-6.0.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e8113639f3296706fbac34a30813929e29247718e88173ad849f57ca59754924", size = 4667818, upload-time = "2025-09-22T04:01:19.688Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f1/ca/31fb37f99f37f1536c133476674c10b577e409c0a624384147653e38baf2/lxml-6.0.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a8bef9b9825fa8bc816a6e641bb67219489229ebc648be422af695f6e7a4fa7f", size = 4950807, upload-time = "2025-09-22T04:01:21.487Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/da/87/f6cb9442e4bada8aab5ae7e1046264f62fdbeaa6e3f6211b93f4c0dd97f1/lxml-6.0.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:65ea18d710fd14e0186c2f973dc60bb52039a275f82d3c44a0e42b43440ea534", size = 5109179, upload-time = "2025-09-22T04:01:23.32Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c8/20/a7760713e65888db79bbae4f6146a6ae5c04e4a204a3c48896c408cd6ed2/lxml-6.0.2-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c371aa98126a0d4c739ca93ceffa0fd7a5d732e3ac66a46e74339acd4d334564", size = 5023044, upload-time = "2025-09-22T04:01:25.118Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a2/b0/7e64e0460fcb36471899f75831509098f3fd7cd02a3833ac517433cb4f8f/lxml-6.0.2-cp312-cp312-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:700efd30c0fa1a3581d80a748157397559396090a51d306ea59a70020223d16f", size = 5359685, upload-time = "2025-09-22T04:01:27.398Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b9/e1/e5df362e9ca4e2f48ed6411bd4b3a0ae737cc842e96877f5bf9428055ab4/lxml-6.0.2-cp312-cp312-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c33e66d44fe60e72397b487ee92e01da0d09ba2d66df8eae42d77b6d06e5eba0", size = 5654127, upload-time = "2025-09-22T04:01:29.629Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c6/d1/232b3309a02d60f11e71857778bfcd4acbdb86c07db8260caf7d008b08f8/lxml-6.0.2-cp312-cp312-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:90a345bbeaf9d0587a3aaffb7006aa39ccb6ff0e96a57286c0cb2fd1520ea192", size = 5253958, upload-time = "2025-09-22T04:01:31.535Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/35/35/d955a070994725c4f7d80583a96cab9c107c57a125b20bb5f708fe941011/lxml-6.0.2-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:064fdadaf7a21af3ed1dcaa106b854077fbeada827c18f72aec9346847cd65d0", size = 4711541, upload-time = "2025-09-22T04:01:33.801Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1e/be/667d17363b38a78c4bd63cfd4b4632029fd68d2c2dc81f25ce9eb5224dd5/lxml-6.0.2-cp312-cp312-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fbc74f42c3525ac4ffa4b89cbdd00057b6196bcefe8bce794abd42d33a018092", size = 5267426, upload-time = "2025-09-22T04:01:35.639Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ea/47/62c70aa4a1c26569bc958c9ca86af2bb4e1f614e8c04fb2989833874f7ae/lxml-6.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6ddff43f702905a4e32bc24f3f2e2edfe0f8fde3277d481bffb709a4cced7a1f", size = 5064917, upload-time = "2025-09-22T04:01:37.448Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bd/55/6ceddaca353ebd0f1908ef712c597f8570cc9c58130dbb89903198e441fd/lxml-6.0.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:6da5185951d72e6f5352166e3da7b0dc27aa70bd1090b0eb3f7f7212b53f1bb8", size = 4788795, upload-time = "2025-09-22T04:01:39.165Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cf/e8/fd63e15da5e3fd4c2146f8bbb3c14e94ab850589beab88e547b2dbce22e1/lxml-6.0.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:57a86e1ebb4020a38d295c04fc79603c7899e0df71588043eb218722dabc087f", size = 5676759, upload-time = "2025-09-22T04:01:41.506Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/76/47/b3ec58dc5c374697f5ba37412cd2728f427d056315d124dd4b61da381877/lxml-6.0.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:2047d8234fe735ab77802ce5f2297e410ff40f5238aec569ad7c8e163d7b19a6", size = 5255666, upload-time = "2025-09-22T04:01:43.363Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/19/93/03ba725df4c3d72afd9596eef4a37a837ce8e4806010569bedfcd2cb68fd/lxml-6.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6f91fd2b2ea15a6800c8e24418c0775a1694eefc011392da73bc6cef2623b322", size = 5277989, upload-time = "2025-09-22T04:01:45.215Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c6/80/c06de80bfce881d0ad738576f243911fccf992687ae09fd80b734712b39c/lxml-6.0.2-cp312-cp312-win32.whl", hash = "sha256:3ae2ce7d6fedfb3414a2b6c5e20b249c4c607f72cb8d2bb7cc9c6ec7c6f4e849", size = 3611456, upload-time = "2025-09-22T04:01:48.243Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f7/d7/0cdfb6c3e30893463fb3d1e52bc5f5f99684a03c29a0b6b605cfae879cd5/lxml-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:72c87e5ee4e58a8354fb9c7c84cbf95a1c8236c127a5d1b7683f04bed8361e1f", size = 4011793, upload-time = "2025-09-22T04:01:50.042Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ea/7b/93c73c67db235931527301ed3785f849c78991e2e34f3fd9a6663ffda4c5/lxml-6.0.2-cp312-cp312-win_arm64.whl", hash = "sha256:61cb10eeb95570153e0c0e554f58df92ecf5109f75eacad4a95baa709e26c3d6", size = 3672836, upload-time = "2025-09-22T04:01:52.145Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/53/fd/4e8f0540608977aea078bf6d79f128e0e2c2bba8af1acf775c30baa70460/lxml-6.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:9b33d21594afab46f37ae58dfadd06636f154923c4e8a4d754b0127554eb2e77", size = 8648494, upload-time = "2025-09-22T04:01:54.242Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5d/f4/2a94a3d3dfd6c6b433501b8d470a1960a20ecce93245cf2db1706adf6c19/lxml-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:6c8963287d7a4c5c9a432ff487c52e9c5618667179c18a204bdedb27310f022f", size = 4661146, upload-time = "2025-09-22T04:01:56.282Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/25/2e/4efa677fa6b322013035d38016f6ae859d06cac67437ca7dc708a6af7028/lxml-6.0.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1941354d92699fb5ffe6ed7b32f9649e43c2feb4b97205f75866f7d21aa91452", size = 4946932, upload-time = "2025-09-22T04:01:58.989Z" },
|
||||
|
|
@ -242,18 +203,51 @@ wheels = [
|
|||
{ url = "https://files.pythonhosted.org/packages/0f/85/86766dfebfa87bea0ab78e9ff7a4b4b45225df4b4d3b8cc3c03c5cd68464/lxml-6.0.2-cp314-cp314t-win32.whl", hash = "sha256:da08e7bb297b04e893d91087df19638dc7a6bb858a954b0cc2b9f5053c922312", size = 3911420, upload-time = "2025-09-22T04:03:32.198Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fe/1a/b248b355834c8e32614650b8008c69ffeb0ceb149c793961dd8c0b991bb3/lxml-6.0.2-cp314-cp314t-win_amd64.whl", hash = "sha256:252a22982dca42f6155125ac76d3432e548a7625d56f5a273ee78a5057216eca", size = 4406837, upload-time = "2025-09-22T04:03:34.027Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/92/aa/df863bcc39c5e0946263454aba394de8a9084dbaff8ad143846b0d844739/lxml-6.0.2-cp314-cp314t-win_arm64.whl", hash = "sha256:bb4c1847b303835d89d785a18801a883436cdfd5dc3d62947f9c49e24f0f5a2c", size = 3822205, upload-time = "2025-09-22T04:03:36.249Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e7/9c/780c9a8fce3f04690b374f72f41306866b0400b9d0fdf3e17aaa37887eed/lxml-6.0.2-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:e748d4cf8fef2526bb2a589a417eba0c8674e29ffcb570ce2ceca44f1e567bf6", size = 3939264, upload-time = "2025-09-22T04:04:32.892Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f5/5a/1ab260c00adf645d8bf7dec7f920f744b032f69130c681302821d5debea6/lxml-6.0.2-pp310-pypy310_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:4ddb1049fa0579d0cbd00503ad8c58b9ab34d1254c77bc6a5576d96ec7853dba", size = 4216435, upload-time = "2025-09-22T04:04:34.907Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f2/37/565f3b3d7ffede22874b6d86be1a1763d00f4ea9fc5b9b6ccb11e4ec8612/lxml-6.0.2-pp310-pypy310_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cb233f9c95f83707dae461b12b720c1af9c28c2d19208e1be03387222151daf5", size = 4325913, upload-time = "2025-09-22T04:04:37.205Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/22/ec/f3a1b169b2fb9d03467e2e3c0c752ea30e993be440a068b125fc7dd248b0/lxml-6.0.2-pp310-pypy310_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bc456d04db0515ce3320d714a1eac7a97774ff0849e7718b492d957da4631dd4", size = 4269357, upload-time = "2025-09-22T04:04:39.322Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/77/a2/585a28fe3e67daa1cf2f06f34490d556d121c25d500b10082a7db96e3bcd/lxml-6.0.2-pp310-pypy310_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2613e67de13d619fd283d58bda40bff0ee07739f624ffee8b13b631abf33083d", size = 4412295, upload-time = "2025-09-22T04:04:41.647Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7b/d9/a57dd8bcebd7c69386c20263830d4fa72d27e6b72a229ef7a48e88952d9a/lxml-6.0.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:24a8e756c982c001ca8d59e87c80c4d9dcd4d9b44a4cbeb8d9be4482c514d41d", size = 3516913, upload-time = "2025-09-22T04:04:43.602Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0b/11/29d08bc103a62c0eba8016e7ed5aeebbf1e4312e83b0b1648dd203b0e87d/lxml-6.0.2-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:1c06035eafa8404b5cf475bb37a9f6088b0aca288d4ccc9d69389750d5543700", size = 3949829, upload-time = "2025-09-22T04:04:45.608Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/12/b3/52ab9a3b31e5ab8238da241baa19eec44d2ab426532441ee607165aebb52/lxml-6.0.2-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c7d13103045de1bdd6fe5d61802565f1a3537d70cd3abf596aa0af62761921ee", size = 4226277, upload-time = "2025-09-22T04:04:47.754Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a0/33/1eaf780c1baad88224611df13b1c2a9dfa460b526cacfe769103ff50d845/lxml-6.0.2-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0a3c150a95fbe5ac91de323aa756219ef9cf7fde5a3f00e2281e30f33fa5fa4f", size = 4330433, upload-time = "2025-09-22T04:04:49.907Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7a/c1/27428a2ff348e994ab4f8777d3a0ad510b6b92d37718e5887d2da99952a2/lxml-6.0.2-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:60fa43be34f78bebb27812ed90f1925ec99560b0fa1decdb7d12b84d857d31e9", size = 4272119, upload-time = "2025-09-22T04:04:51.801Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f0/d0/3020fa12bcec4ab62f97aab026d57c2f0cfd480a558758d9ca233bb6a79d/lxml-6.0.2-pp311-pypy311_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:21c73b476d3cfe836be731225ec3421fa2f048d84f6df6a8e70433dff1376d5a", size = 4417314, upload-time = "2025-09-22T04:04:55.024Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6c/77/d7f491cbc05303ac6801651aabeb262d43f319288c1ea96c66b1d2692ff3/lxml-6.0.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:27220da5be049e936c3aca06f174e8827ca6445a4353a1995584311487fc4e3e", size = 3518768, upload-time = "2025-09-22T04:04:57.097Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mypy-extensions"
|
||||
version = "1.1.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "packaging"
|
||||
version = "26.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/65/ee/299d360cdc32edc7d2cf530f3accf79c4fca01e96ffc950d8a52213bd8e4/packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", size = 143416, upload-time = "2026-01-21T20:50:39.064Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pathspec"
|
||||
version = "1.0.4"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/fa/36/e27608899f9b8d4dff0617b2d9ab17ca5608956ca44461ac14ac48b44015/pathspec-1.0.4.tar.gz", hash = "sha256:0210e2ae8a21a9137c0d470578cb0e595af87edaa6ebf12ff176f14a02e0e645", size = 131200, upload-time = "2026-01-27T03:59:46.938Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/ef/3c/2c197d226f9ea224a9ab8d197933f9da0ae0aac5b6e0f884e2b8d9c8e9f7/pathspec-1.0.4-py3-none-any.whl", hash = "sha256:fb6ae2fd4e7c921a165808a552060e722767cfa526f99ca5156ed2ce45a5c723", size = 55206, upload-time = "2026-01-27T03:59:45.137Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "platformdirs"
|
||||
version = "4.9.4"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/19/56/8d4c30c8a1d07013911a8fdbd8f89440ef9f08d07a1b50ab8ca8be5a20f9/platformdirs-4.9.4.tar.gz", hash = "sha256:1ec356301b7dc906d83f371c8f487070e99d3ccf9e501686456394622a01a934", size = 28737, upload-time = "2026-03-05T18:34:13.271Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/63/d7/97f7e3a6abb67d8080dd406fd4df842c2be0efaf712d1c899c32a075027c/platformdirs-4.9.4-py3-none-any.whl", hash = "sha256:68a9a4619a666ea6439f2ff250c12a853cd1cbd5158d258bd824a7df6be2f868", size = 21216, upload-time = "2026-03-05T18:34:12.172Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pluggy"
|
||||
version = "1.6.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -267,6 +261,13 @@ dependencies = [
|
|||
{ name = "requests" },
|
||||
]
|
||||
|
||||
[package.dev-dependencies]
|
||||
dev = [
|
||||
{ name = "black" },
|
||||
{ name = "isort" },
|
||||
{ name = "pytest" },
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "beautifulsoup4" },
|
||||
|
|
@ -275,6 +276,38 @@ requires-dist = [
|
|||
{ name = "requests" },
|
||||
]
|
||||
|
||||
[package.metadata.requires-dev]
|
||||
dev = [
|
||||
{ name = "black", specifier = ">=24.10.0,<25.0.0" },
|
||||
{ name = "isort", specifier = ">=5.13.2,<6.0.0" },
|
||||
{ name = "pytest", specifier = ">=8.3.5,<9.0.0" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pygments"
|
||||
version = "2.19.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pytest"
|
||||
version = "8.4.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
||||
{ name = "iniconfig" },
|
||||
{ name = "packaging" },
|
||||
{ name = "pluggy" },
|
||||
{ name = "pygments" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/a3/5c/00a0e072241553e1a7496d638deababa67c5058571567b92a7eaa258397c/pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01", size = 1519618, upload-time = "2025-09-04T14:34:22.711Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/a8/a4/20da314d277121d6534b3a980b29035dcd51e6744bd79075a6ce8fa4eb8d/pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79", size = 365750, upload-time = "2025-09-04T14:34:20.226Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "python-dateutil"
|
||||
version = "2.9.0.post0"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue