import project from david with uv
This commit is contained in:
commit
8056704736
11 changed files with 1530 additions and 0 deletions
7
.gitignore
vendored
Normal file
7
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
*.tar
|
||||||
|
*.ini
|
||||||
|
feed/
|
||||||
|
logs/
|
||||||
|
dev/
|
||||||
|
__pycache__
|
||||||
|
NOTES
|
||||||
4
pangea.sh
Executable file
4
pangea.sh
Executable file
|
|
@ -0,0 +1,4 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
cd "$(dirname "$0")"
|
||||||
|
uv run pygea
|
||||||
0
pygea/__init__.py
Normal file
0
pygea/__init__.py
Normal file
55
pygea/main.py
Normal file
55
pygea/main.py
Normal file
|
|
@ -0,0 +1,55 @@
|
||||||
|
"""Pygea main entry point"""
|
||||||
|
import hashlib
|
||||||
|
from pygea.pangeafeed import PangeaFeed
|
||||||
|
from pygea.pexception import PangeaServiceException
|
||||||
|
|
||||||
|
def main():
|
||||||
|
# Feeds are generated for a single, specified, domain
|
||||||
|
domain = 'www.martinoticias.com'
|
||||||
|
|
||||||
|
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"
|
||||||
|
}
|
||||||
|
|
||||||
|
# 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
|
||||||
|
try:
|
||||||
|
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)
|
||||||
|
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)
|
||||||
|
print("feed for {} output to sub-directory {}".format(cat_tuple[0], feed_subdir))
|
||||||
|
except PangeaServiceException as error:
|
||||||
|
print(error)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
192
pygea/pangeafeed.py
Normal file
192
pygea/pangeafeed.py
Normal file
|
|
@ -0,0 +1,192 @@
|
||||||
|
# pylint: disable-msg=C0103
|
||||||
|
# pylint: disable-msg=C0201
|
||||||
|
"""
|
||||||
|
- * -
|
||||||
|
Generate a custom RSS feed from Pangea, for a specific domain, with one or more
|
||||||
|
categories or content filters and an optional supplied content-type.
|
||||||
|
- * -
|
||||||
|
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
from datetime import datetime
|
||||||
|
from feedgen.feed import FeedGenerator
|
||||||
|
from pygea import pangeaservice
|
||||||
|
from pygea import pexception
|
||||||
|
from pygea import 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')
|
||||||
|
|
||||||
|
class PangeaFeed():
|
||||||
|
|
||||||
|
_domain = None
|
||||||
|
_categories = None
|
||||||
|
_content_type = 'articles' # default
|
||||||
|
|
||||||
|
def __init__(self, domain, kw_args):
|
||||||
|
try:
|
||||||
|
self._ps = pangeaservice.PangeaService(domain)
|
||||||
|
except pexception.PangeaServiceException as error:
|
||||||
|
raise error
|
||||||
|
|
||||||
|
self._domain = domain
|
||||||
|
if kw_args.get('categories'):
|
||||||
|
self._categories = kw_args['categories']
|
||||||
|
else:
|
||||||
|
raise pexception.PangeaServiceException("ERROR: At least one category or content-query is required")
|
||||||
|
|
||||||
|
if kw_args.get('default_content_type'):
|
||||||
|
if kw_args['default_content_type'] not in self._ps.content_types():
|
||||||
|
raise pexception.PangeaServiceException("{} is not a valid content type".format(kw_args['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:
|
||||||
|
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:
|
||||||
|
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)
|
||||||
|
else:
|
||||||
|
# cat as actually a free-form query string to be used no article content
|
||||||
|
jbody = self._ps.query_content(cat, opt_args)
|
||||||
|
if len(jbody) == 0:
|
||||||
|
if VERBOSE:
|
||||||
|
print("no articles available for {} [command: {}] [category/query: '{}'])".format(self._domain, self._content_type, cat))
|
||||||
|
continue
|
||||||
|
|
||||||
|
if VERBOSE:
|
||||||
|
print ("{} articles added from category/query '{}'".format(str(len(jbody)), cat))
|
||||||
|
|
||||||
|
for art in jbody:
|
||||||
|
self._full_article_list.append(art)
|
||||||
|
|
||||||
|
def generate_feed(self):
|
||||||
|
#
|
||||||
|
# Get preparatory information from the domain's homepage. Most characteristics
|
||||||
|
# of the RSS Channel information are acquired from the homepage metadata.
|
||||||
|
#
|
||||||
|
md = utilities.get_webpage_metadata('https://' + self._domain)
|
||||||
|
|
||||||
|
fg = FeedGenerator()
|
||||||
|
self._fg = fg
|
||||||
|
|
||||||
|
#
|
||||||
|
# build the RSS <channel> element
|
||||||
|
#
|
||||||
|
fg.id(utilities.hash_site_metadata(md))
|
||||||
|
fg.title(self._content_type + ' from ' + md['og:site_name'])
|
||||||
|
fg.link(href=md['og:url'], rel='alternate')
|
||||||
|
fg.description(self._content_type + ' from ' + self._domain + " (" + md['description'] + ")")
|
||||||
|
|
||||||
|
#
|
||||||
|
# NOTE: the parameters required for <image> in the <channel> are different
|
||||||
|
# from <image> in an <item>
|
||||||
|
fg.image(url=md['og:image'], title=md['og:site_name'], link=md['og:url'])
|
||||||
|
|
||||||
|
#
|
||||||
|
# Multiple categories/keywords are allowed in the RSS Channel
|
||||||
|
keywords = md['keywords']
|
||||||
|
categories = keywords.split(',')
|
||||||
|
sch = 'https://' + self._domain + '/'
|
||||||
|
for name in categories:
|
||||||
|
fg.category(term=name, scheme=sch, label=name)
|
||||||
|
|
||||||
|
fg.language(md['language'])
|
||||||
|
fg.generator('Guardian Project Pangea CMS Crawler 1.0')
|
||||||
|
fg.webMaster('support@guardianproject.info')
|
||||||
|
fg.ttl(60)
|
||||||
|
|
||||||
|
datetime_obj = datetime.now()
|
||||||
|
formatted_time = datetime_obj.strftime('%a, %d %b %Y %H:%M:%S %Z')
|
||||||
|
fg.lastBuildDate(formatted_time + '+0000')
|
||||||
|
|
||||||
|
#
|
||||||
|
# Build the <item> elements for each <item> and add each item to the RSS Channel
|
||||||
|
#
|
||||||
|
media_extension_loaded = False
|
||||||
|
for article in self._full_article_list:
|
||||||
|
try:
|
||||||
|
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:
|
||||||
|
print(error)
|
||||||
|
print("article with id [{}] may no longer exist in Pangea".format(str(article['id'])))
|
||||||
|
continue
|
||||||
|
|
||||||
|
fe = fg.add_entry()
|
||||||
|
fe.title(rss_article['title'])
|
||||||
|
fe.link({'href': rss_article['link']})
|
||||||
|
fe.guid(rss_article['guid'])
|
||||||
|
fe.pubDate(rss_article['pubDate'])
|
||||||
|
fe.content(rss_article['content'])
|
||||||
|
if rss_article.get('summary'):
|
||||||
|
fe.description(rss_article['summary'])
|
||||||
|
|
||||||
|
if rss_article.get('enclosure'):
|
||||||
|
enc_md = rss_article['enclosure']
|
||||||
|
if enc_md.get('type'):
|
||||||
|
fe.enclosure(
|
||||||
|
url=enc_md['url'],
|
||||||
|
type=enc_md['type'],
|
||||||
|
length=enc_md['length'])
|
||||||
|
else:
|
||||||
|
fe.enclosure(url=enc_md['url'])
|
||||||
|
|
||||||
|
if rss_article.get('media_content'):
|
||||||
|
#
|
||||||
|
# special handling for the RSS media extension
|
||||||
|
#
|
||||||
|
if not media_extension_loaded:
|
||||||
|
fg.load_extension('media')
|
||||||
|
media_extension_loaded = True
|
||||||
|
if VERBOSE: print("media extension loaded")
|
||||||
|
|
||||||
|
mc_md = rss_article['media_content']
|
||||||
|
if mc_md.get('medium'):
|
||||||
|
fe.media.content(
|
||||||
|
url=mc_md['url'],
|
||||||
|
type=mc_md['type'],
|
||||||
|
fileSize=mc_md['fileSize'],
|
||||||
|
medium=mc_md['medium'])
|
||||||
|
else:
|
||||||
|
fe.media.content(url=mc_md['url'])
|
||||||
|
|
||||||
|
|
||||||
|
def disgorge(self, subdirectory = None):
|
||||||
|
#
|
||||||
|
# Output the RSS feed as appropriate
|
||||||
|
#
|
||||||
|
if OUTPUT_TO_FILE 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)))
|
||||||
|
sys.exit(1)
|
||||||
|
if VERBOSE: print("output written to {}".format(ofile))
|
||||||
|
else:
|
||||||
|
print(self._fg.rss_str(extensions=True, pretty=True))
|
||||||
657
pygea/pangeaservice.py
Normal file
657
pygea/pangeaservice.py
Normal file
|
|
@ -0,0 +1,657 @@
|
||||||
|
"""
|
||||||
|
- * -
|
||||||
|
Interface to USAGM Pangea Content Management System API
|
||||||
|
|
||||||
|
This implementation is a subset of API functions, focusing on the eventual
|
||||||
|
creation of RSS (or other) data streams from article selections
|
||||||
|
|
||||||
|
Pangea Documentation:
|
||||||
|
https://showcase.pangea-cms.com/a/pangea-api-methods-and-models/29663096.html
|
||||||
|
|
||||||
|
:copyright: 2024, David Oliver <david@guardianproject.info>
|
||||||
|
:license: http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License
|
||||||
|
- * -
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import re
|
||||||
|
import hashlib
|
||||||
|
import urllib.parse
|
||||||
|
from datetime import datetime, timezone, timedelta
|
||||||
|
import requests
|
||||||
|
from dateutil.parser import *
|
||||||
|
from pygea import utilities
|
||||||
|
from pygea import pexception
|
||||||
|
from pygea import plogger
|
||||||
|
|
||||||
|
class PangeaService:
|
||||||
|
""" Interface to the Pangea API """
|
||||||
|
|
||||||
|
_configuration_file_name = 'pygea.ini'
|
||||||
|
_api_path = '/api2/'
|
||||||
|
_api_key = None
|
||||||
|
|
||||||
|
# Pangea and RSS time format
|
||||||
|
TIME_FMT = "%Y-%m-%dT%H:%M:%S.%f" # ex. 2024-08-02T11:46:28.673
|
||||||
|
TIME_FMT_I = "%Y-%m-%dT%H:%M:%S" # ex. 2024-08-02T11:46:28
|
||||||
|
RFC822_FMT = "%a, %d %B %Y %H:%M:%S %z"
|
||||||
|
|
||||||
|
# API commands - commands commented out are valid in the API but NOT SUPPORTED HERE
|
||||||
|
_commands_list = [
|
||||||
|
"articledetail",
|
||||||
|
"articles",
|
||||||
|
"audioclips",
|
||||||
|
#"audioscheduler",
|
||||||
|
"author",
|
||||||
|
#"blogitem",
|
||||||
|
"breakingnews",
|
||||||
|
#"comment",
|
||||||
|
"config",
|
||||||
|
#"documentdetail",
|
||||||
|
"empty",
|
||||||
|
#"factcheckdetail",
|
||||||
|
#"htmlwidget",
|
||||||
|
#"infographicdetail",
|
||||||
|
#"liveblogs",
|
||||||
|
#"livestream",
|
||||||
|
"mostpopular",
|
||||||
|
#"polldetail",
|
||||||
|
#"quizdetail",
|
||||||
|
"search",
|
||||||
|
"test",
|
||||||
|
"topstories",
|
||||||
|
"videoclips",
|
||||||
|
#"videoscheduler",
|
||||||
|
#"widget",
|
||||||
|
"zone"
|
||||||
|
]
|
||||||
|
|
||||||
|
# Position-indexed content category names
|
||||||
|
_category_types_list = [
|
||||||
|
'none', # 0 internally
|
||||||
|
'content', # 1 internally
|
||||||
|
'audio', # 2 internally
|
||||||
|
'content+audio', # 3 internally; compound type 1+2
|
||||||
|
'media', # 4 internally
|
||||||
|
'content+media', # 5 internally; compound type 1+4
|
||||||
|
'audio+media' # 6 internally; compound type 2+4
|
||||||
|
]
|
||||||
|
|
||||||
|
# Content types (in the editorial sense)
|
||||||
|
# Note these also map to commands in _commands_list
|
||||||
|
_content_types_list = [
|
||||||
|
'articles',
|
||||||
|
'audioclips',
|
||||||
|
'videoclips',
|
||||||
|
'breakingnews',
|
||||||
|
'mostpopular',
|
||||||
|
'topstories'
|
||||||
|
]
|
||||||
|
|
||||||
|
# How to format content
|
||||||
|
# (we WILL NOT use these in combination, as defined in the API)
|
||||||
|
_content_options = {
|
||||||
|
'WTF_0': 0, # Returns basically what is in database
|
||||||
|
'TEXT_ONLY': 1, # Removes all html keeping text only
|
||||||
|
'WTF_1': 2, # Returns tags as they would be displayed on the page
|
||||||
|
'MOBILE_1': 4, # Returns html as for mobile/rss feeds without
|
||||||
|
# additional stripping
|
||||||
|
'MOBILE_2': 8, # Returns html as for mobile/rss feeds with stripping
|
||||||
|
# some html that is not supported
|
||||||
|
'MOBILE_3': 16, # Returns html as for mobile/rss feeds with some extra
|
||||||
|
# html tags stripped
|
||||||
|
'WTF_2': 32, # Same as for Feeds + replaces recognized links with
|
||||||
|
# internal links and wraps recognized images inside tags
|
||||||
|
'XML_TX': 64, # Used with Feeds to apply xsl transformation
|
||||||
|
'JSON': 128 # Generates json structured content
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def __init__(self, domain, key=None, verbose=False):
|
||||||
|
self._logger = plogger.PangeaServiceLogger()
|
||||||
|
|
||||||
|
#
|
||||||
|
# resolve API key: PYGEA_API_KEY env var takes precedence over pygea.ini
|
||||||
|
#
|
||||||
|
self._api_key = utilities.get_api_key()
|
||||||
|
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"
|
||||||
|
)
|
||||||
|
|
||||||
|
#
|
||||||
|
# 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._domain = domain
|
||||||
|
|
||||||
|
# config file overrides on invocation
|
||||||
|
if key is not None:
|
||||||
|
self._api_key = key
|
||||||
|
|
||||||
|
if verbose:
|
||||||
|
self._verbose_p = verbose
|
||||||
|
if self._verbose_p:
|
||||||
|
print('verbose output')
|
||||||
|
#
|
||||||
|
# These two dictionaries index the category information
|
||||||
|
# _all_categories is indexed by category name; _rev_categories is indexed by id
|
||||||
|
#
|
||||||
|
self._all_categories = { }
|
||||||
|
self._rev_categories = {'0': 'none'}
|
||||||
|
|
||||||
|
# Acquire the categories registered for the supplied domain
|
||||||
|
# Invokes an API call!
|
||||||
|
self.get_categories()
|
||||||
|
|
||||||
|
#
|
||||||
|
# Setters
|
||||||
|
#
|
||||||
|
|
||||||
|
def set_domain(self, value):
|
||||||
|
""" Sets the USAGM Internet domain name from which content is acquired """
|
||||||
|
self._domain = value
|
||||||
|
|
||||||
|
# Reset the category dictionaries
|
||||||
|
self._all_categories = { }
|
||||||
|
self._rev_categories = {'0': 'none'}
|
||||||
|
|
||||||
|
# Acquire the categories registered for the supplied domain (API call)
|
||||||
|
self.get_categories()
|
||||||
|
|
||||||
|
|
||||||
|
def set_api_key(self, key):
|
||||||
|
""" Sets the API key that allows access to the API """
|
||||||
|
self._api_key = key
|
||||||
|
|
||||||
|
#
|
||||||
|
# Additional getter and checker methods
|
||||||
|
#
|
||||||
|
|
||||||
|
def content_types(self):
|
||||||
|
""" Return full list of content types. """
|
||||||
|
return self._content_types_list
|
||||||
|
|
||||||
|
|
||||||
|
def content_type_name(self, type_index):
|
||||||
|
""" Returns name of a content type given its index. """
|
||||||
|
if type_index > len(self._content_types_list):
|
||||||
|
return False
|
||||||
|
return self._content_types_list[type_index]
|
||||||
|
|
||||||
|
|
||||||
|
def commands(self):
|
||||||
|
""" Return the list of possible commands. """
|
||||||
|
return self._commands_list
|
||||||
|
|
||||||
|
|
||||||
|
def category_types(self):
|
||||||
|
"""Return of list of possible category types. """
|
||||||
|
return self._category_types_list
|
||||||
|
|
||||||
|
|
||||||
|
def category_info(self, category_name):
|
||||||
|
""" Return rich information about a category. """
|
||||||
|
if self._all_categories.get(category_name):
|
||||||
|
return self._all_categories[category_name]
|
||||||
|
return None
|
||||||
|
|
||||||
|
def content_options(self):
|
||||||
|
""" Return the dictionary of content format options. """
|
||||||
|
return self._content_options
|
||||||
|
|
||||||
|
|
||||||
|
def is_valid_command(self, cmd):
|
||||||
|
""" Test if the provided command is valid and implemented. """
|
||||||
|
return self._is_implemented(cmd)
|
||||||
|
|
||||||
|
|
||||||
|
def is_valid_category(self, category_name):
|
||||||
|
""" Test if a provided category name is valid.
|
||||||
|
|
||||||
|
NOTE: Categories are unique on a per-domain basis, so they are retrieve
|
||||||
|
via the API when this class is instantiated. There are no "generic"
|
||||||
|
categories that apply to all domains.
|
||||||
|
."""
|
||||||
|
keys = self._all_categories.keys()
|
||||||
|
if category_name in keys:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
#
|
||||||
|
# Article formatters
|
||||||
|
#
|
||||||
|
|
||||||
|
def rss_article_from_pangea_article(self, article):
|
||||||
|
"""
|
||||||
|
Use this method to convert an API-returned articledetail definition
|
||||||
|
to an RSS-appropriate definition.
|
||||||
|
|
||||||
|
This method succeeds using the bare article definition, but will be
|
||||||
|
absent the content field and other descriptors. Enclosures are
|
||||||
|
returned, however.
|
||||||
|
"""
|
||||||
|
|
||||||
|
rss = { }
|
||||||
|
|
||||||
|
sh = hashlib.sha256()
|
||||||
|
sh.update(article['url'].encode('utf8'))
|
||||||
|
rss['guid'] = sh.hexdigest()
|
||||||
|
rss['title'] = article['title']
|
||||||
|
rss['link'] = article['url']
|
||||||
|
|
||||||
|
if article.get('introduction'):
|
||||||
|
rss['summary'] = article['introduction']
|
||||||
|
|
||||||
|
if article.get('authors'):
|
||||||
|
as_str = ''
|
||||||
|
for auth in article['authors']:
|
||||||
|
as_str += auth['lastname'] + ", " + auth['firstname'] + ";"
|
||||||
|
if len(article['authors']) > 1:
|
||||||
|
as_str = as_str[0: (len(as_str) - 2)]
|
||||||
|
rss['authors'] = as_str
|
||||||
|
|
||||||
|
if article.get('image'):
|
||||||
|
# Seek the enclosure details from the image's server
|
||||||
|
metadata = utilities.get_media_metadata(article['image'])
|
||||||
|
if metadata:
|
||||||
|
rss['enclosure'] = {
|
||||||
|
'url': article['image'],
|
||||||
|
'type': metadata['content_type'],
|
||||||
|
'length': metadata['content_length']
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
rss['enclosure'] = {'url': article['image']}
|
||||||
|
|
||||||
|
if rss.get('enclosure'):
|
||||||
|
if self._verbose_p:
|
||||||
|
print(
|
||||||
|
"article contains an enclosure:\n"
|
||||||
|
+ json.dumps(rss['enclosure'], indent=4))
|
||||||
|
|
||||||
|
# 'audioclips' and 'videoclips' occasionally have no text content
|
||||||
|
if article.get('content'):
|
||||||
|
rss['content'] = article['content']
|
||||||
|
else:
|
||||||
|
rss['content'] = ''
|
||||||
|
|
||||||
|
# all articles are required to have one category (their 'zone')
|
||||||
|
if bool(article.get('zone')):
|
||||||
|
zone_id = article['zone']
|
||||||
|
if isinstance(zone_id, int):
|
||||||
|
zone_id = str(zone_id)
|
||||||
|
|
||||||
|
if self._rev_categories.get('zone_id'):
|
||||||
|
rss['categories'] = self._rev_categories[zone_id]
|
||||||
|
elif article.get('zoneTitle'):
|
||||||
|
rss['categories'] = article['zoneTitle']
|
||||||
|
|
||||||
|
# Pangea time is always in GMT
|
||||||
|
# Pangea time is formatted as: 2024-07-31T11:46:28.673
|
||||||
|
# (though occasionally: 2024-07-31T11:46:28)
|
||||||
|
# Convert to RSS time (RFC822)
|
||||||
|
if not article.get('pubDate'):
|
||||||
|
datetime_obj = datetime.now(timezone.utc)
|
||||||
|
else:
|
||||||
|
if re.match('.*?([.][0-9]+)$', article['pubDate']):
|
||||||
|
datetime_obj = datetime.strptime(article['pubDate'], self.TIME_FMT)
|
||||||
|
else:
|
||||||
|
datetime_obj = datetime.strptime(article['pubDate'], self.TIME_FMT_I)
|
||||||
|
|
||||||
|
formatted_time = datetime_obj.strftime(self.RFC822_FMT)
|
||||||
|
rss['pubDate'] = formatted_time + '+0000'
|
||||||
|
|
||||||
|
# Media types
|
||||||
|
if bool(article.get('videos')):
|
||||||
|
if len(article['videos']) > 0:
|
||||||
|
url = article['videos'][0]['url']
|
||||||
|
metadata = utilities.get_media_metadata(url)
|
||||||
|
if metadata:
|
||||||
|
rss['media_content'] = {
|
||||||
|
'url': url,
|
||||||
|
'type': metadata['content_type'],
|
||||||
|
'fileSize': metadata['content_length'],
|
||||||
|
'medium': 'video'
|
||||||
|
}
|
||||||
|
if self._verbose_p: print("article contains video media:\n"
|
||||||
|
+ json.dumps(rss['media_content'], indent=4))
|
||||||
|
else:
|
||||||
|
rss['media_content'] = {'url': url}
|
||||||
|
|
||||||
|
if bool(article.get('audios')):
|
||||||
|
if len(article['audios']) > 0:
|
||||||
|
url = article['audios'][0]['url']
|
||||||
|
metadata = utilities.get_media_metadata(url)
|
||||||
|
if metadata:
|
||||||
|
rss['media_content'] = {
|
||||||
|
'url': url,
|
||||||
|
'type': metadata['content_type'],
|
||||||
|
'fileSize': metadata['content_length'],
|
||||||
|
'medium': 'audio'
|
||||||
|
}
|
||||||
|
if self._verbose_p: print("article contains audio media:\n"
|
||||||
|
+ json.dumps(rss['media_content'], indent=4))
|
||||||
|
else:
|
||||||
|
rss['media_content'] = {'url': url}
|
||||||
|
|
||||||
|
return rss
|
||||||
|
|
||||||
|
#
|
||||||
|
# Public API methods
|
||||||
|
#
|
||||||
|
|
||||||
|
def test_pangea_interface(self):
|
||||||
|
""" TESTING Basic connectivity test """
|
||||||
|
return self._retrieve_content('test')
|
||||||
|
|
||||||
|
|
||||||
|
def empty(self):
|
||||||
|
"""
|
||||||
|
TESTING Returns nothing but, if command formatted properly, with proper API
|
||||||
|
key, HTTP status will be 200
|
||||||
|
"""
|
||||||
|
res = self._retrieve_content('empty')
|
||||||
|
return res
|
||||||
|
|
||||||
|
|
||||||
|
def config(self):
|
||||||
|
""" TESTING Returns configuration information about the API """
|
||||||
|
return self._retrieve_content('config')
|
||||||
|
|
||||||
|
|
||||||
|
def get_content(self, content_type, optional_args_kw = None):
|
||||||
|
"""
|
||||||
|
Use this method to get articles by content type, subset by a specific category
|
||||||
|
as supplied. See API docs for additional API parameters that can be specified
|
||||||
|
to reduce the volume of articles returned.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
res = self._retrieve_content(content_type, optional_args_kw)
|
||||||
|
except pexception.PangeaServiceException as e:
|
||||||
|
raise pexception.PangeaServiceException(str(e)) from e
|
||||||
|
|
||||||
|
# because Pangea does not uniformly apply 'count' and 'daycount' parameters
|
||||||
|
# to all content generation, we'll do that here (unless we're told to ignore).
|
||||||
|
if optional_args_kw is not None:
|
||||||
|
if optional_args_kw.get('filter_date') is not None:
|
||||||
|
if optional_args_kw.get('filter_date') is False:
|
||||||
|
return res
|
||||||
|
|
||||||
|
return self._threshold(res)
|
||||||
|
|
||||||
|
|
||||||
|
def query_content(self, query, optional_args_kw = None):
|
||||||
|
"""
|
||||||
|
Use this method to get articles based on textual search.
|
||||||
|
See API docs for additional API parameters that can be specified
|
||||||
|
to reduce the volume of articles returned. Alternatively, see
|
||||||
|
docs for the 'pageNumber' parameter to handling a search returning
|
||||||
|
many articles (only query/search supports this parameter).
|
||||||
|
"""
|
||||||
|
# make the topic/category URL-safe
|
||||||
|
if optional_args_kw is None:
|
||||||
|
optional_args_kw = {}
|
||||||
|
optional_args_kw['q'] = urllib.parse.quote_plus(query)
|
||||||
|
|
||||||
|
try:
|
||||||
|
res = self._retrieve_content('search', optional_args_kw)
|
||||||
|
except pexception.PangeaServiceException as e:
|
||||||
|
raise pexception.PangeaServiceException(str(e)) from e
|
||||||
|
|
||||||
|
#
|
||||||
|
# because Pangea does not uniformly apply 'count' and 'daycount' parameters
|
||||||
|
# to all content generation, we'll do that here (unless we're told to ignore).
|
||||||
|
if optional_args_kw.get('filter_date') is not None:
|
||||||
|
if optional_args_kw.get('filter_date') is False:
|
||||||
|
return res
|
||||||
|
|
||||||
|
return self._threshold(res)
|
||||||
|
|
||||||
|
def get_article(self, article_id, optional_args_kw = None):
|
||||||
|
"""
|
||||||
|
Use this method to get all the detail for a given article (typically
|
||||||
|
required to do anything useful).
|
||||||
|
"""
|
||||||
|
if optional_args_kw is None:
|
||||||
|
optional_args_kw = {}
|
||||||
|
if 'MediaData' not in optional_args_kw.keys():
|
||||||
|
optional_args_kw['MediaData'] = 'true'
|
||||||
|
|
||||||
|
optional_args_kw['itemid'] = article_id
|
||||||
|
try:
|
||||||
|
res = self._retrieve_content('articles', optional_args_kw)
|
||||||
|
except pexception.PangeaServiceException as e:
|
||||||
|
raise pexception.PangeaServiceException(str(e)) from e
|
||||||
|
|
||||||
|
#print(json.dumps(res, indent=4))
|
||||||
|
return res
|
||||||
|
|
||||||
|
def get_article_detail(self, article_id, optional_args_kw = None):
|
||||||
|
"""
|
||||||
|
Use this method to get all the detail for a given article (typically
|
||||||
|
required to do anything useful).
|
||||||
|
"""
|
||||||
|
if optional_args_kw is None:
|
||||||
|
optional_args_kw = {}
|
||||||
|
if 'Content' not in optional_args_kw.keys():
|
||||||
|
optional_args_kw['Content'] = 'true'
|
||||||
|
if 'MediaData' not in optional_args_kw.keys():
|
||||||
|
optional_args_kw['MediaData'] = 'true'
|
||||||
|
|
||||||
|
optional_args_kw['itemid'] = article_id
|
||||||
|
try:
|
||||||
|
res = self._retrieve_content('articledetail', optional_args_kw)
|
||||||
|
except pexception.PangeaServiceException as e:
|
||||||
|
raise pexception.PangeaServiceException(str(e)) from e
|
||||||
|
|
||||||
|
#print(json.dumps(res, indent=4))
|
||||||
|
return res
|
||||||
|
|
||||||
|
|
||||||
|
def get_categories(self, types = None):
|
||||||
|
"""
|
||||||
|
Categories are defined on a PER DOMAIN basis, so to assure the user
|
||||||
|
provides a proper category name we need to acquire the full set of
|
||||||
|
categories before we proceed with any queries.
|
||||||
|
"""
|
||||||
|
if len(self._all_categories.keys()) > 0:
|
||||||
|
return self._all_categories
|
||||||
|
|
||||||
|
if types is None:
|
||||||
|
types = 'acm' # get all content types 'a', 'c', 'm' at once
|
||||||
|
|
||||||
|
args = {'type': types}
|
||||||
|
try:
|
||||||
|
url = self._build_url('zone', args)
|
||||||
|
response = requests.get(url, timeout=20)
|
||||||
|
if response.status_code != 200:
|
||||||
|
msg = "HTP request to {} failed with status code [{}]".format(self._domain, str(response.status_code))
|
||||||
|
self._logger.error(msg)
|
||||||
|
raise pexception.PangeaServiceException(msg)
|
||||||
|
a_cat = json.loads(response.text)
|
||||||
|
except pexception.PangeaServiceException as e:
|
||||||
|
raise pexception.PangeaServiceException(str(e)) from e
|
||||||
|
|
||||||
|
all_keys = self._all_categories.keys()
|
||||||
|
for c in a_cat:
|
||||||
|
if not c['name'] in all_keys:
|
||||||
|
self._all_categories[c['name']] = c
|
||||||
|
self._rev_categories[str(c['id'])] = c['name']
|
||||||
|
|
||||||
|
if c['type'] >= len(self._category_types_list):
|
||||||
|
msg = "ERROR: unknown type: {} on id [{}], name: {}".format(c['type'], str(c['id']), c['name'])
|
||||||
|
self._logger.warning(msg)
|
||||||
|
raise pexception.PangeaServiceException(msg)
|
||||||
|
|
||||||
|
return json.loads(json.dumps(self._all_categories))
|
||||||
|
|
||||||
|
#
|
||||||
|
# Private methods
|
||||||
|
#
|
||||||
|
|
||||||
|
def _boolean_string(self, boolean_value):
|
||||||
|
""" Convert a boolean to a string for the API """
|
||||||
|
if boolean_value is True:
|
||||||
|
return 'true'
|
||||||
|
return 'false'
|
||||||
|
|
||||||
|
|
||||||
|
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)
|
||||||
|
self._logger.error(msg)
|
||||||
|
raise pexception.PangeaServiceException(msg)
|
||||||
|
if command == 'empty':
|
||||||
|
return json.loads('[]')
|
||||||
|
|
||||||
|
return json.loads(response.text)
|
||||||
|
|
||||||
|
|
||||||
|
def _is_implemented(self, cmd):
|
||||||
|
""" Test if a provided string references an actual command """
|
||||||
|
if cmd in self._commands_list:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def _threshold(self, articles):
|
||||||
|
""" Assure article-count and oldest-article settings are obeyed. Turns out,
|
||||||
|
only a few API commands accept these arguments, though in general our
|
||||||
|
usage of the API requires it to be consistent.
|
||||||
|
"""
|
||||||
|
output = []
|
||||||
|
article_count = 0
|
||||||
|
delta = timedelta(days=self._oldest_article)
|
||||||
|
for blob in articles:
|
||||||
|
if article_count <= self._max_articles:
|
||||||
|
#
|
||||||
|
# pubDate may contain milliseconds, or not
|
||||||
|
#
|
||||||
|
if re.match('.*?([.][0-9]+)$', blob['pubDate']):
|
||||||
|
dt = datetime.strptime(blob['pubDate'], self.TIME_FMT)
|
||||||
|
dt.replace(microsecond=0)
|
||||||
|
else:
|
||||||
|
dt = datetime.strptime(blob['pubDate'], self.TIME_FMT_I)
|
||||||
|
old_dt = datetime.now() - delta
|
||||||
|
if dt < old_dt:
|
||||||
|
if self._verbose_p:
|
||||||
|
print("article with ID {} is too old [{}]".format(str(blob['id']), dt.strftime(self.TIME_FMT_I)))
|
||||||
|
else:
|
||||||
|
article_count += 1
|
||||||
|
output.append(blob)
|
||||||
|
|
||||||
|
if self._verbose_p & (len(output) < len(articles)):
|
||||||
|
print("request returned {} articles; newest {} processed".format(str(len(articles)), str(len(output))))
|
||||||
|
|
||||||
|
reordered = output[::-1]
|
||||||
|
return reordered
|
||||||
|
|
||||||
|
|
||||||
|
def _build_url(self, cmd, args_kw = None):
|
||||||
|
""" Construct a properly-formatted Pangea API URL """
|
||||||
|
if not self._is_implemented(cmd):
|
||||||
|
msg = "ERROR: command [{}] NOT IMPLEMENTED".format(cmd)
|
||||||
|
self._logger.error(msg)
|
||||||
|
raise pexception.PangeaServiceException(msg)
|
||||||
|
|
||||||
|
if not self._api_key:
|
||||||
|
msg = "ERROR: no API key supplied (check config file {})".format(self._configuration_file_name)
|
||||||
|
self._logger.error(msg)
|
||||||
|
raise pexception.PangeaServiceException(msg)
|
||||||
|
|
||||||
|
if not args_kw:
|
||||||
|
args_kw = {}
|
||||||
|
|
||||||
|
# this switch verifies (and/or completes) the argument array
|
||||||
|
match cmd:
|
||||||
|
#simple commands
|
||||||
|
case 'empty' | 'test':
|
||||||
|
pass
|
||||||
|
|
||||||
|
# search
|
||||||
|
case 'search':
|
||||||
|
if 'q' not in args_kw.keys():
|
||||||
|
msg = "ERROR: [{}] requires parameter 'q'".format(cmd)
|
||||||
|
self._logger.error(msg)
|
||||||
|
raise pexception.PangeaServiceException(msg)
|
||||||
|
|
||||||
|
if 'Authors' not in args_kw.keys():
|
||||||
|
args_kw['Authors'] = self._boolean_string(self._authors_p)
|
||||||
|
|
||||||
|
if 'count' not in args_kw.keys():
|
||||||
|
args_kw['count'] = self._max_articles
|
||||||
|
|
||||||
|
if 'daycount' not in args_kw.keys():
|
||||||
|
args_kw['daycount'] = self._oldest_article
|
||||||
|
|
||||||
|
# single-item/detail commands
|
||||||
|
case 'articledetail' | 'blogitem' | 'comment' | 'author' | 'documentdetail' | 'factcheckdetail' | 'infographicdetail' | 'polldetail' | 'quizdetail':
|
||||||
|
if 'itemid' not in args_kw.keys():
|
||||||
|
msg = "ERROR: [{}] command requires arg 'itemid'".format(cmd)
|
||||||
|
self._logger.error(msg)
|
||||||
|
raise pexception.PangeaServiceException(msg)
|
||||||
|
|
||||||
|
if 'Content' not in args_kw.keys():
|
||||||
|
args_kw['Content'] = self._boolean_string(self._content_inc_p)
|
||||||
|
|
||||||
|
if 'Authors' not in args_kw.keys():
|
||||||
|
args_kw['Authors'] = self._boolean_string(self._authors_p)
|
||||||
|
|
||||||
|
if 'html' not in args_kw.keys():
|
||||||
|
args_kw['html'] = self._content_options[self._content_format]
|
||||||
|
|
||||||
|
case 'authorid':
|
||||||
|
if 'authorid' not in args_kw.keys():
|
||||||
|
msg = "ERROR: [{}] command requires arg 'authorid'".format(cmd)
|
||||||
|
self._logger.error(msg)
|
||||||
|
raise pexception.PangeaServiceException(msg)
|
||||||
|
|
||||||
|
case 'zone':
|
||||||
|
if ('zoneid' not in args_kw.keys()) & ('type' not in args_kw.keys()):
|
||||||
|
msg = "ERROR: [{}] command requires args 'zoneid' or 'type'".format(cmd)
|
||||||
|
self._logger.error(msg)
|
||||||
|
raise pexception.PangeaServiceException(msg)
|
||||||
|
|
||||||
|
# content commands
|
||||||
|
case 'articles' | 'audioclips' | 'videoclips' | 'breakingnews' | 'mostpopular' | 'topstories' | 'blogitem':
|
||||||
|
if 'Authors' not in args_kw.keys():
|
||||||
|
args_kw['Authors'] = self._boolean_string(self._authors_p)
|
||||||
|
|
||||||
|
if 'count' not in args_kw.keys():
|
||||||
|
args_kw['count'] = self._max_articles
|
||||||
|
|
||||||
|
if 'daycount' not in args_kw.keys():
|
||||||
|
args_kw['daycount'] = self._oldest_article
|
||||||
|
|
||||||
|
# base for all types of command (apikey needs to be first arg)
|
||||||
|
url = "https://" + self._domain + self._api_path + cmd + '?apikey=' + self._api_key
|
||||||
|
|
||||||
|
# process the arg array to finish construction of the URL
|
||||||
|
for key, value in args_kw.items():
|
||||||
|
# remove this one
|
||||||
|
if key == 'filter_date':
|
||||||
|
continue
|
||||||
|
|
||||||
|
if not isinstance(key, str):
|
||||||
|
key = str(key)
|
||||||
|
if not isinstance(value, str):
|
||||||
|
value = str(value)
|
||||||
|
|
||||||
|
url += '&' + key + '=' + value
|
||||||
|
|
||||||
|
if self._verbose_p:
|
||||||
|
print("URL for request: " + url)
|
||||||
|
|
||||||
|
return url
|
||||||
6
pygea/pexception.py
Normal file
6
pygea/pexception.py
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
"""
|
||||||
|
A less-generic Exception for the Pangea API Service
|
||||||
|
"""
|
||||||
|
|
||||||
|
class PangeaServiceException(Exception):
|
||||||
|
""" An Exception specific to this API """
|
||||||
56
pygea/plogger.py
Normal file
56
pygea/plogger.py
Normal file
|
|
@ -0,0 +1,56 @@
|
||||||
|
"""
|
||||||
|
Logger for the Pangea API Service
|
||||||
|
"""
|
||||||
|
import logging
|
||||||
|
from pygea import utilities
|
||||||
|
|
||||||
|
class PangeaServiceLogger:
|
||||||
|
"""
|
||||||
|
Mostly, so that someone can replace this with a production logger later.
|
||||||
|
"""
|
||||||
|
|
||||||
|
_configuration_file_name = 'pygea.ini'
|
||||||
|
_levels = {
|
||||||
|
"NOTSET": 0,
|
||||||
|
"DEBUG": 10,
|
||||||
|
"INFO": 20,
|
||||||
|
"WARNING": 30,
|
||||||
|
"ERROR": 40,
|
||||||
|
"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'
|
||||||
|
|
||||||
|
self._logger = logging.getLogger('PangeaLogger')
|
||||||
|
self._logger.propagate = False
|
||||||
|
logging.basicConfig(
|
||||||
|
filename=lf,
|
||||||
|
level=self._levels[dl],
|
||||||
|
format='[%(asctime)s] %(levelname)s: %(message)s')
|
||||||
|
|
||||||
|
def debug(self, message):
|
||||||
|
""" Debug message """
|
||||||
|
self._logger.debug(message)
|
||||||
|
|
||||||
|
def info(self, message):
|
||||||
|
""" Info message """
|
||||||
|
self._logger.info(message)
|
||||||
|
|
||||||
|
def warning(self, message):
|
||||||
|
""" Warning message """
|
||||||
|
self._logger.warning(message)
|
||||||
|
|
||||||
|
def error(self, message):
|
||||||
|
""" Error message """
|
||||||
|
self._logger.error(message)
|
||||||
|
|
||||||
|
def critical(self, message):
|
||||||
|
""" Critical message """
|
||||||
|
self._logger.critical(message)
|
||||||
196
pygea/utilities.py
Normal file
196
pygea/utilities.py
Normal file
|
|
@ -0,0 +1,196 @@
|
||||||
|
# pylint: disable-msg=C0201
|
||||||
|
"""
|
||||||
|
- * -
|
||||||
|
Utilities for the Pangea CMS Service API
|
||||||
|
|
||||||
|
- * -
|
||||||
|
"""
|
||||||
|
import hashlib
|
||||||
|
import os
|
||||||
|
from urllib.parse import urlparse
|
||||||
|
from configparser import ConfigParser, NoSectionError, NoOptionError
|
||||||
|
import requests
|
||||||
|
from bs4 import BeautifulSoup
|
||||||
|
|
||||||
|
def acquire(url):
|
||||||
|
""" Simple wrapper over the request object. """
|
||||||
|
response = requests.get(url, timeout=20)
|
||||||
|
|
||||||
|
# Check if the request was successful
|
||||||
|
if response.status_code == 200:
|
||||||
|
content = response.text
|
||||||
|
else:
|
||||||
|
print("Failed to retrieve the web page. Status code: " + str(response.status_code))
|
||||||
|
return None
|
||||||
|
|
||||||
|
return content
|
||||||
|
|
||||||
|
def parse_url_elements(url):
|
||||||
|
""" URL hackery - returns domain and Pangea article ID from a provided URL """
|
||||||
|
out = {}
|
||||||
|
|
||||||
|
parts = urlparse(url)
|
||||||
|
out['domain'] = parts.hostname
|
||||||
|
|
||||||
|
# article ID is the file name at the end of the path ('324534.html')
|
||||||
|
more_parts = parts.path.split('/')
|
||||||
|
file = more_parts[len(more_parts)-1]
|
||||||
|
file_parts = file.split('.')
|
||||||
|
out['article_id'] = file_parts[0]
|
||||||
|
|
||||||
|
return out
|
||||||
|
|
||||||
|
def get_webpage_metadata(page_url):
|
||||||
|
""" Get HTML metadata elements from a webpage. """
|
||||||
|
parsed = urlparse(page_url)
|
||||||
|
domain = parsed.netloc
|
||||||
|
#
|
||||||
|
# USAGM websites support the OpenGraph tags which provide most
|
||||||
|
# of the metadata we require.
|
||||||
|
#
|
||||||
|
html_content = acquire(page_url)
|
||||||
|
if html_content == None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
soup = BeautifulSoup(html_content, 'html.parser')
|
||||||
|
meta_tags = soup.find_all('meta')
|
||||||
|
|
||||||
|
metadata = {}
|
||||||
|
for tag in meta_tags:
|
||||||
|
if 'name' in tag.attrs:
|
||||||
|
name = tag.attrs['name']
|
||||||
|
content = tag.attrs.get('content', '')
|
||||||
|
metadata[name] = content
|
||||||
|
elif 'property' in tag.attrs: # For OpenGraph metadata
|
||||||
|
prop = tag.attrs['property']
|
||||||
|
content = tag.attrs.get('content', '')
|
||||||
|
metadata[prop] = content
|
||||||
|
|
||||||
|
# add useful language property
|
||||||
|
html = soup.find_all('html')
|
||||||
|
metadata['language'] = html[0]['lang']
|
||||||
|
|
||||||
|
# add links
|
||||||
|
link_tags = soup.find_all('link')
|
||||||
|
for tag in link_tags:
|
||||||
|
if 'rel' in tag.attrs:
|
||||||
|
#print(json.dumps(tag.attrs, indent=4))
|
||||||
|
if 'alternate' in tag.attrs['rel']:
|
||||||
|
if 'icon' in tag.attrs['rel']:
|
||||||
|
metadata['favicon'] = 'https://' + domain + tag.attrs.get('href')
|
||||||
|
if tag.attrs['rel'][0] == 'canonical':
|
||||||
|
metadata['canonical'] = tag.attrs.get('href')
|
||||||
|
|
||||||
|
return metadata
|
||||||
|
|
||||||
|
def get_media_metadata(image_url):
|
||||||
|
""" Get metadata for media content from website (via response headers). """
|
||||||
|
response = requests.head(image_url, timeout=20)
|
||||||
|
meta = None
|
||||||
|
if response.status_code == 200:
|
||||||
|
meta = {
|
||||||
|
"content_type": response.headers['Content-Type'],
|
||||||
|
"content_length": response.headers['Content-Length']
|
||||||
|
}
|
||||||
|
|
||||||
|
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. """
|
||||||
|
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
|
||||||
|
|
||||||
|
def is_domain_name(domain):
|
||||||
|
""" Does the provided string resemble a domain name? """
|
||||||
|
if any(char in domain for char in "."):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def hash_site_metadata(metadata):
|
||||||
|
""" Create a secure hash of website HTTP meta headers to use as an RSS/ATOM ID. """
|
||||||
|
sh = hashlib.sha256()
|
||||||
|
for key in metadata.keys():
|
||||||
|
sh.update(key.encode('utf8') + metadata[key].encode('utf8'))
|
||||||
|
|
||||||
|
digest = sh.hexdigest()
|
||||||
|
return digest
|
||||||
|
|
||||||
|
def rss_namespace_supported(prop):
|
||||||
|
""" Determine if a provided RSS/XML namespace is valid in the FeedGen RSS package. """
|
||||||
|
supported_namespaces = [
|
||||||
|
'dc',
|
||||||
|
'geo',
|
||||||
|
'gen_entry',
|
||||||
|
'media',
|
||||||
|
'podcast',
|
||||||
|
'podcast_entry',
|
||||||
|
'syndication',
|
||||||
|
'torrent'
|
||||||
|
]
|
||||||
|
if prop in supported_namespaces:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def rss_namespace_for_property(prop):
|
||||||
|
""" Returns the XML namespace for a specified <channel> or <item>
|
||||||
|
property from among a list of the most popular namespace schemes
|
||||||
|
according to:
|
||||||
|
https://www.rssboard.org/news/168/rss-channel-element-usage-stats
|
||||||
|
For an exhaustive list of namespace schemes see:
|
||||||
|
https://validator.w3.org/feed/docs/howto/declare_namespaces.html
|
||||||
|
"""
|
||||||
|
known_namespaces = {
|
||||||
|
'content': 'http://purl.org/rss/1.0/modules/content/', # content
|
||||||
|
'dc': 'http://purl.org/dc/elements/1.1/', # Dublin Core
|
||||||
|
'atom': 'http://www.w3.org/2005/Atom', # ATOM
|
||||||
|
'sy': 'http://purl.org/rss/1.0/modules/syndication/', # Syndication
|
||||||
|
'admin': 'http://webns.net/mvcb/',
|
||||||
|
'feedburner': 'http://rssnamespace.org/feedburner/ext/1.0', # Feedburner
|
||||||
|
'cc': 'http://web.resource.org/cc/', # copyrights
|
||||||
|
'geo': 'http://www.w3.org/2003/01/geo/wgs84_pos#',
|
||||||
|
'opensearch': 'http://a9.com/-/spec/opensearch/1.1/', # OpenSearch
|
||||||
|
'itunes': 'http://www.itunes.com/dtds/podcast-1.0.dtd', # Apple iTunes
|
||||||
|
'blogChannel': 'http://backend.userland.com/blogChannelModule', # BlogChannel
|
||||||
|
'media': 'http://search.yahoo.com/mrss/', # media RSS
|
||||||
|
'icbm': 'http://postneo.com/icbm', # ICBM
|
||||||
|
'cf': 'http://www.microsoft.com/schemas/rss/core/2005', # a Microsoft thing
|
||||||
|
'podcast': 'https://podcastindex.org/namespace/1.0', # Podcast RSS
|
||||||
|
'xhtml': 'http://www.w3.org/1999/xhtml' # XHTML
|
||||||
|
}
|
||||||
|
|
||||||
|
components = prop.split(':')
|
||||||
|
if known_namespaces.get(components[0]):
|
||||||
|
return known_namespaces[components[0]]
|
||||||
|
|
||||||
|
return None
|
||||||
18
pyproject.toml
Normal file
18
pyproject.toml
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
[project]
|
||||||
|
name = "pygea"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = "Pangea RSS feed generator"
|
||||||
|
requires-python = ">=3.10"
|
||||||
|
dependencies = [
|
||||||
|
"requests",
|
||||||
|
"beautifulsoup4",
|
||||||
|
"feedgen",
|
||||||
|
"python-dateutil",
|
||||||
|
]
|
||||||
|
|
||||||
|
[project.scripts]
|
||||||
|
pygea = "pygea.main:main"
|
||||||
|
|
||||||
|
[build-system]
|
||||||
|
requires = ["hatchling"]
|
||||||
|
build-backend = "hatchling.build"
|
||||||
339
uv.lock
generated
Normal file
339
uv.lock
generated
Normal file
|
|
@ -0,0 +1,339 @@
|
||||||
|
version = 1
|
||||||
|
revision = 3
|
||||||
|
requires-python = ">=3.10"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "beautifulsoup4"
|
||||||
|
version = "4.14.3"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "soupsieve" },
|
||||||
|
{ name = "typing-extensions" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/c3/b0/1c6a16426d389813b48d95e26898aff79abbde42ad353958ad95cc8c9b21/beautifulsoup4-4.14.3.tar.gz", hash = "sha256:6292b1c5186d356bba669ef9f7f051757099565ad9ada5dd630bd9de5fa7fb86", size = 627737, upload-time = "2025-11-30T15:08:26.084Z" }
|
||||||
|
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 = "certifi"
|
||||||
|
version = "2026.1.4"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/e0/2d/a891ca51311197f6ad14a7ef42e2399f36cf2f9bd44752b3dc4eab60fdc5/certifi-2026.1.4.tar.gz", hash = "sha256:ac726dd470482006e014ad384921ed6438c457018f4b3d204aea4281258b2120", size = 154268, upload-time = "2026-01-04T02:42:41.825Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e6/ad/3cc14f097111b4de0040c83a525973216457bbeeb63739ef1ed275c1c021/certifi-2026.1.4-py3-none-any.whl", hash = "sha256:9943707519e4add1115f44c2bc244f782c0249876bf51b6599fee1ffbedd685c", size = 152900, upload-time = "2026-01-04T02:42:40.15Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "charset-normalizer"
|
||||||
|
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" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/91/ed/9706e4070682d1cc219050b6048bfd293ccf67b3d4f5a4f39207453d4b99/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328", size = 161346, upload-time = "2025-10-14T04:41:16.738Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d5/0d/031f0d95e4972901a2f6f09ef055751805ff541511dc1252ba3ca1f80cf5/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede", size = 158874, upload-time = "2025-10-14T04:41:17.923Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f5/83/6ab5883f57c9c801ce5e5677242328aa45592be8a00644310a008d04f922/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894", size = 153076, upload-time = "2025-10-14T04:41:19.106Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/75/1e/5ff781ddf5260e387d6419959ee89ef13878229732732ee73cdae01800f2/charset_normalizer-3.4.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1", size = 150601, upload-time = "2025-10-14T04:41:20.245Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d7/57/71be810965493d3510a6ca79b90c19e48696fb1ff964da319334b12677f0/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490", size = 150376, upload-time = "2025-10-14T04:41:21.398Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e5/d5/c3d057a78c181d007014feb7e9f2e65905a6c4ef182c0ddf0de2924edd65/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44", size = 144825, upload-time = "2025-10-14T04:41:22.583Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e6/8c/d0406294828d4976f275ffbe66f00266c4b3136b7506941d87c00cab5272/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133", size = 162583, upload-time = "2025-10-14T04:41:23.754Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d7/24/e2aa1f18c8f15c4c0e932d9287b8609dd30ad56dbe41d926bd846e22fb8d/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3", size = 150366, upload-time = "2025-10-14T04:41:25.27Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e4/5b/1e6160c7739aad1e2df054300cc618b06bf784a7a164b0f238360721ab86/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e", size = 160300, upload-time = "2025-10-14T04:41:26.725Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7a/10/f882167cd207fbdd743e55534d5d9620e095089d176d55cb22d5322f2afd/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc", size = 154465, upload-time = "2025-10-14T04:41:28.322Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/89/66/c7a9e1b7429be72123441bfdbaf2bc13faab3f90b933f664db506dea5915/charset_normalizer-3.4.4-cp313-cp313-win32.whl", hash = "sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac", size = 99404, upload-time = "2025-10-14T04:41:29.95Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c4/26/b9924fa27db384bdcd97ab83b4f0a8058d96ad9626ead570674d5e737d90/charset_normalizer-3.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14", size = 107092, upload-time = "2025-10-14T04:41:31.188Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/af/8f/3ed4bfa0c0c72a7ca17f0380cd9e4dd842b09f664e780c13cff1dcf2ef1b/charset_normalizer-3.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2", size = 100408, upload-time = "2025-10-14T04:41:32.624Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2a/35/7051599bd493e62411d6ede36fd5af83a38f37c4767b92884df7301db25d/charset_normalizer-3.4.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd", size = 207746, upload-time = "2025-10-14T04:41:33.773Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/10/9a/97c8d48ef10d6cd4fcead2415523221624bf58bcf68a802721a6bc807c8f/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb", size = 147889, upload-time = "2025-10-14T04:41:34.897Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/10/bf/979224a919a1b606c82bd2c5fa49b5c6d5727aa47b4312bb27b1734f53cd/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e", size = 143641, upload-time = "2025-10-14T04:41:36.116Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ba/33/0ad65587441fc730dc7bd90e9716b30b4702dc7b617e6ba4997dc8651495/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14", size = 160779, upload-time = "2025-10-14T04:41:37.229Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/67/ed/331d6b249259ee71ddea93f6f2f0a56cfebd46938bde6fcc6f7b9a3d0e09/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191", size = 159035, upload-time = "2025-10-14T04:41:38.368Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/67/ff/f6b948ca32e4f2a4576aa129d8bed61f2e0543bf9f5f2b7fc3758ed005c9/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838", size = 152542, upload-time = "2025-10-14T04:41:39.862Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/16/85/276033dcbcc369eb176594de22728541a925b2632f9716428c851b149e83/charset_normalizer-3.4.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6", size = 149524, upload-time = "2025-10-14T04:41:41.319Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9e/f2/6a2a1f722b6aba37050e626530a46a68f74e63683947a8acff92569f979a/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e", size = 150395, upload-time = "2025-10-14T04:41:42.539Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/60/bb/2186cb2f2bbaea6338cad15ce23a67f9b0672929744381e28b0592676824/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c", size = 143680, upload-time = "2025-10-14T04:41:43.661Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7d/a5/bf6f13b772fbb2a90360eb620d52ed8f796f3c5caee8398c3b2eb7b1c60d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090", size = 162045, upload-time = "2025-10-14T04:41:44.821Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/df/c5/d1be898bf0dc3ef9030c3825e5d3b83f2c528d207d246cbabe245966808d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152", size = 149687, upload-time = "2025-10-14T04:41:46.442Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a5/42/90c1f7b9341eef50c8a1cb3f098ac43b0508413f33affd762855f67a410e/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828", size = 160014, upload-time = "2025-10-14T04:41:47.631Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/76/be/4d3ee471e8145d12795ab655ece37baed0929462a86e72372fd25859047c/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec", size = 154044, upload-time = "2025-10-14T04:41:48.81Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b0/6f/8f7af07237c34a1defe7defc565a9bc1807762f672c0fde711a4b22bf9c0/charset_normalizer-3.4.4-cp314-cp314-win32.whl", hash = "sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9", size = 99940, upload-time = "2025-10-14T04:41:49.946Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/4b/51/8ade005e5ca5b0d80fb4aff72a3775b325bdc3d27408c8113811a7cbe640/charset_normalizer-3.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c", size = 107104, upload-time = "2025-10-14T04:41:51.051Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/da/5f/6b8f83a55bb8278772c5ae54a577f3099025f9ade59d0136ac24a0df4bde/charset_normalizer-3.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2", size = 100743, upload-time = "2025-10-14T04:41:52.122Z" },
|
||||||
|
{ 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 = "feedgen"
|
||||||
|
version = "1.0.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "lxml" },
|
||||||
|
{ name = "python-dateutil" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/6b/59/be0a6f852b5dfbf19e6c8e962c8f41407697f9f52a7902250ed98683ae89/feedgen-1.0.0.tar.gz", hash = "sha256:d9bd51c3b5e956a2a52998c3708c4d2c729f2fcc311188e1e5d3b9726393546a", size = 258496, upload-time = "2023-12-25T18:04:08.421Z" }
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "idna"
|
||||||
|
version = "3.11"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" }
|
||||||
|
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 = "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" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ce/0f/526e78a6d38d109fdbaa5049c62e1d32fdd70c75fb61c4eadf3045d3d124/lxml-6.0.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:bb2f6ca0ae2d983ded09357b84af659c954722bbf04dea98030064996d156048", size = 5100060, upload-time = "2025-09-22T04:02:00.812Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/81/76/99de58d81fa702cc0ea7edae4f4640416c2062813a00ff24bd70ac1d9c9b/lxml-6.0.2-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eb2a12d704f180a902d7fa778c6d71f36ceb7b0d317f34cdc76a5d05aa1dd1df", size = 5019000, upload-time = "2025-09-22T04:02:02.671Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b5/35/9e57d25482bc9a9882cb0037fdb9cc18f4b79d85df94fa9d2a89562f1d25/lxml-6.0.2-cp313-cp313-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:6ec0e3f745021bfed19c456647f0298d60a24c9ff86d9d051f52b509663feeb1", size = 5348496, upload-time = "2025-09-22T04:02:04.904Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a6/8e/cb99bd0b83ccc3e8f0f528e9aa1f7a9965dfec08c617070c5db8d63a87ce/lxml-6.0.2-cp313-cp313-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:846ae9a12d54e368933b9759052d6206a9e8b250291109c48e350c1f1f49d916", size = 5643779, upload-time = "2025-09-22T04:02:06.689Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d0/34/9e591954939276bb679b73773836c6684c22e56d05980e31d52a9a8deb18/lxml-6.0.2-cp313-cp313-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ef9266d2aa545d7374938fb5c484531ef5a2ec7f2d573e62f8ce722c735685fd", size = 5244072, upload-time = "2025-09-22T04:02:08.587Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8d/27/b29ff065f9aaca443ee377aff699714fcbffb371b4fce5ac4ca759e436d5/lxml-6.0.2-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:4077b7c79f31755df33b795dc12119cb557a0106bfdab0d2c2d97bd3cf3dffa6", size = 4718675, upload-time = "2025-09-22T04:02:10.783Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2b/9f/f756f9c2cd27caa1a6ef8c32ae47aadea697f5c2c6d07b0dae133c244fbe/lxml-6.0.2-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a7c5d5e5f1081955358533be077166ee97ed2571d6a66bdba6ec2f609a715d1a", size = 5255171, upload-time = "2025-09-22T04:02:12.631Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/61/46/bb85ea42d2cb1bd8395484fd72f38e3389611aa496ac7772da9205bbda0e/lxml-6.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:8f8d0cbd0674ee89863a523e6994ac25fd5be9c8486acfc3e5ccea679bad2679", size = 5057175, upload-time = "2025-09-22T04:02:14.718Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/95/0c/443fc476dcc8e41577f0af70458c50fe299a97bb6b7505bb1ae09aa7f9ac/lxml-6.0.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:2cbcbf6d6e924c28f04a43f3b6f6e272312a090f269eff68a2982e13e5d57659", size = 4785688, upload-time = "2025-09-22T04:02:16.957Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/48/78/6ef0b359d45bb9697bc5a626e1992fa5d27aa3f8004b137b2314793b50a0/lxml-6.0.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:dfb874cfa53340009af6bdd7e54ebc0d21012a60a4e65d927c2e477112e63484", size = 5660655, upload-time = "2025-09-22T04:02:18.815Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ff/ea/e1d33808f386bc1339d08c0dcada6e4712d4ed8e93fcad5f057070b7988a/lxml-6.0.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:fb8dae0b6b8b7f9e96c26fdd8121522ce5de9bb5538010870bd538683d30e9a2", size = 5247695, upload-time = "2025-09-22T04:02:20.593Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/4f/47/eba75dfd8183673725255247a603b4ad606f4ae657b60c6c145b381697da/lxml-6.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:358d9adae670b63e95bc59747c72f4dc97c9ec58881d4627fe0120da0f90d314", size = 5269841, upload-time = "2025-09-22T04:02:22.489Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/76/04/5c5e2b8577bc936e219becb2e98cdb1aca14a4921a12995b9d0c523502ae/lxml-6.0.2-cp313-cp313-win32.whl", hash = "sha256:e8cd2415f372e7e5a789d743d133ae474290a90b9023197fd78f32e2dc6873e2", size = 3610700, upload-time = "2025-09-22T04:02:24.465Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/fe/0a/4643ccc6bb8b143e9f9640aa54e38255f9d3b45feb2cbe7ae2ca47e8782e/lxml-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:b30d46379644fbfc3ab81f8f82ae4de55179414651f110a1514f0b1f8f6cb2d7", size = 4010347, upload-time = "2025-09-22T04:02:26.286Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/31/ef/dcf1d29c3f530577f61e5fe2f1bd72929acf779953668a8a47a479ae6f26/lxml-6.0.2-cp313-cp313-win_arm64.whl", hash = "sha256:13dcecc9946dca97b11b7c40d29fba63b55ab4170d3c0cf8c0c164343b9bfdcf", size = 3671248, upload-time = "2025-09-22T04:02:27.918Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/03/15/d4a377b385ab693ce97b472fe0c77c2b16ec79590e688b3ccc71fba19884/lxml-6.0.2-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:b0c732aa23de8f8aec23f4b580d1e52905ef468afb4abeafd3fec77042abb6fe", size = 8659801, upload-time = "2025-09-22T04:02:30.113Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c8/e8/c128e37589463668794d503afaeb003987373c5f94d667124ffd8078bbd9/lxml-6.0.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:4468e3b83e10e0317a89a33d28f7aeba1caa4d1a6fd457d115dd4ffe90c5931d", size = 4659403, upload-time = "2025-09-22T04:02:32.119Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/00/ce/74903904339decdf7da7847bb5741fc98a5451b42fc419a86c0c13d26fe2/lxml-6.0.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:abd44571493973bad4598a3be7e1d807ed45aa2adaf7ab92ab7c62609569b17d", size = 4966974, upload-time = "2025-09-22T04:02:34.155Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1f/d3/131dec79ce61c5567fecf82515bd9bc36395df42501b50f7f7f3bd065df0/lxml-6.0.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:370cd78d5855cfbffd57c422851f7d3864e6ae72d0da615fca4dad8c45d375a5", size = 5102953, upload-time = "2025-09-22T04:02:36.054Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3a/ea/a43ba9bb750d4ffdd885f2cd333572f5bb900cd2408b67fdda07e85978a0/lxml-6.0.2-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:901e3b4219fa04ef766885fb40fa516a71662a4c61b80c94d25336b4934b71c0", size = 5055054, upload-time = "2025-09-22T04:02:38.154Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/60/23/6885b451636ae286c34628f70a7ed1fcc759f8d9ad382d132e1c8d3d9bfd/lxml-6.0.2-cp314-cp314-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:a4bf42d2e4cf52c28cc1812d62426b9503cdb0c87a6de81442626aa7d69707ba", size = 5352421, upload-time = "2025-09-22T04:02:40.413Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/48/5b/fc2ddfc94ddbe3eebb8e9af6e3fd65e2feba4967f6a4e9683875c394c2d8/lxml-6.0.2-cp314-cp314-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b2c7fdaa4d7c3d886a42534adec7cfac73860b89b4e5298752f60aa5984641a0", size = 5673684, upload-time = "2025-09-22T04:02:42.288Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/29/9c/47293c58cc91769130fbf85531280e8cc7868f7fbb6d92f4670071b9cb3e/lxml-6.0.2-cp314-cp314-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:98a5e1660dc7de2200b00d53fa00bcd3c35a3608c305d45a7bbcaf29fa16e83d", size = 5252463, upload-time = "2025-09-22T04:02:44.165Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9b/da/ba6eceb830c762b48e711ded880d7e3e89fc6c7323e587c36540b6b23c6b/lxml-6.0.2-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:dc051506c30b609238d79eda75ee9cab3e520570ec8219844a72a46020901e37", size = 4698437, upload-time = "2025-09-22T04:02:46.524Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a5/24/7be3f82cb7990b89118d944b619e53c656c97dc89c28cfb143fdb7cd6f4d/lxml-6.0.2-cp314-cp314-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8799481bbdd212470d17513a54d568f44416db01250f49449647b5ab5b5dccb9", size = 5269890, upload-time = "2025-09-22T04:02:48.812Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1b/bd/dcfb9ea1e16c665efd7538fc5d5c34071276ce9220e234217682e7d2c4a5/lxml-6.0.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9261bb77c2dab42f3ecd9103951aeca2c40277701eb7e912c545c1b16e0e4917", size = 5097185, upload-time = "2025-09-22T04:02:50.746Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/21/04/a60b0ff9314736316f28316b694bccbbabe100f8483ad83852d77fc7468e/lxml-6.0.2-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:65ac4a01aba353cfa6d5725b95d7aed6356ddc0a3cd734de00124d285b04b64f", size = 4745895, upload-time = "2025-09-22T04:02:52.968Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d6/bd/7d54bd1846e5a310d9c715921c5faa71cf5c0853372adf78aee70c8d7aa2/lxml-6.0.2-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:b22a07cbb82fea98f8a2fd814f3d1811ff9ed76d0fc6abc84eb21527596e7cc8", size = 5695246, upload-time = "2025-09-22T04:02:54.798Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/fd/32/5643d6ab947bc371da21323acb2a6e603cedbe71cb4c99c8254289ab6f4e/lxml-6.0.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:d759cdd7f3e055d6bc8d9bec3ad905227b2e4c785dc16c372eb5b5e83123f48a", size = 5260797, upload-time = "2025-09-22T04:02:57.058Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/33/da/34c1ec4cff1eea7d0b4cd44af8411806ed943141804ac9c5d565302afb78/lxml-6.0.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:945da35a48d193d27c188037a05fec5492937f66fb1958c24fc761fb9d40d43c", size = 5277404, upload-time = "2025-09-22T04:02:58.966Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/82/57/4eca3e31e54dc89e2c3507e1cd411074a17565fa5ffc437c4ae0a00d439e/lxml-6.0.2-cp314-cp314-win32.whl", hash = "sha256:be3aaa60da67e6153eb15715cc2e19091af5dc75faef8b8a585aea372507384b", size = 3670072, upload-time = "2025-09-22T04:03:38.05Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e3/e0/c96cf13eccd20c9421ba910304dae0f619724dcf1702864fd59dd386404d/lxml-6.0.2-cp314-cp314-win_amd64.whl", hash = "sha256:fa25afbadead523f7001caf0c2382afd272c315a033a7b06336da2637d92d6ed", size = 4080617, upload-time = "2025-09-22T04:03:39.835Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d5/5d/b3f03e22b3d38d6f188ef044900a9b29b2fe0aebb94625ce9fe244011d34/lxml-6.0.2-cp314-cp314-win_arm64.whl", hash = "sha256:063eccf89df5b24e361b123e257e437f9e9878f425ee9aae3144c77faf6da6d8", size = 3754930, upload-time = "2025-09-22T04:03:41.565Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5e/5c/42c2c4c03554580708fc738d13414801f340c04c3eff90d8d2d227145275/lxml-6.0.2-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:6162a86d86893d63084faaf4ff937b3daea233e3682fb4474db07395794fa80d", size = 8910380, upload-time = "2025-09-22T04:03:01.645Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/bf/4f/12df843e3e10d18d468a7557058f8d3733e8b6e12401f30b1ef29360740f/lxml-6.0.2-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:414aaa94e974e23a3e92e7ca5b97d10c0cf37b6481f50911032c69eeb3991bba", size = 4775632, upload-time = "2025-09-22T04:03:03.814Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e4/0c/9dc31e6c2d0d418483cbcb469d1f5a582a1cd00a1f4081953d44051f3c50/lxml-6.0.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:48461bd21625458dd01e14e2c38dd0aea69addc3c4f960c30d9f59d7f93be601", size = 4975171, upload-time = "2025-09-22T04:03:05.651Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e7/2b/9b870c6ca24c841bdd887504808f0417aa9d8d564114689266f19ddf29c8/lxml-6.0.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:25fcc59afc57d527cfc78a58f40ab4c9b8fd096a9a3f964d2781ffb6eb33f4ed", size = 5110109, upload-time = "2025-09-22T04:03:07.452Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/bf/0c/4f5f2a4dd319a178912751564471355d9019e220c20d7db3fb8307ed8582/lxml-6.0.2-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5179c60288204e6ddde3f774a93350177e08876eaf3ab78aa3a3649d43eb7d37", size = 5041061, upload-time = "2025-09-22T04:03:09.297Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/12/64/554eed290365267671fe001a20d72d14f468ae4e6acef1e179b039436967/lxml-6.0.2-cp314-cp314t-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:967aab75434de148ec80597b75062d8123cadf2943fb4281f385141e18b21338", size = 5306233, upload-time = "2025-09-22T04:03:11.651Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7a/31/1d748aa275e71802ad9722df32a7a35034246b42c0ecdd8235412c3396ef/lxml-6.0.2-cp314-cp314t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d100fcc8930d697c6561156c6810ab4a508fb264c8b6779e6e61e2ed5e7558f9", size = 5604739, upload-time = "2025-09-22T04:03:13.592Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8f/41/2c11916bcac09ed561adccacceaedd2bf0e0b25b297ea92aab99fd03d0fa/lxml-6.0.2-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2ca59e7e13e5981175b8b3e4ab84d7da57993eeff53c07764dcebda0d0e64ecd", size = 5225119, upload-time = "2025-09-22T04:03:15.408Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/99/05/4e5c2873d8f17aa018e6afde417c80cc5d0c33be4854cce3ef5670c49367/lxml-6.0.2-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:957448ac63a42e2e49531b9d6c0fa449a1970dbc32467aaad46f11545be9af1d", size = 4633665, upload-time = "2025-09-22T04:03:17.262Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0f/c9/dcc2da1bebd6275cdc723b515f93edf548b82f36a5458cca3578bc899332/lxml-6.0.2-cp314-cp314t-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b7fc49c37f1786284b12af63152fe1d0990722497e2d5817acfe7a877522f9a9", size = 5234997, upload-time = "2025-09-22T04:03:19.14Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9c/e2/5172e4e7468afca64a37b81dba152fc5d90e30f9c83c7c3213d6a02a5ce4/lxml-6.0.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e19e0643cc936a22e837f79d01a550678da8377d7d801a14487c10c34ee49c7e", size = 5090957, upload-time = "2025-09-22T04:03:21.436Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a5/b3/15461fd3e5cd4ddcb7938b87fc20b14ab113b92312fc97afe65cd7c85de1/lxml-6.0.2-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:1db01e5cf14345628e0cbe71067204db658e2fb8e51e7f33631f5f4735fefd8d", size = 4764372, upload-time = "2025-09-22T04:03:23.27Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/05/33/f310b987c8bf9e61c4dd8e8035c416bd3230098f5e3cfa69fc4232de7059/lxml-6.0.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:875c6b5ab39ad5291588aed6925fac99d0097af0dd62f33c7b43736043d4a2ec", size = 5634653, upload-time = "2025-09-22T04:03:25.767Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/70/ff/51c80e75e0bc9382158133bdcf4e339b5886c6ee2418b5199b3f1a61ed6d/lxml-6.0.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:cdcbed9ad19da81c480dfd6dd161886db6096083c9938ead313d94b30aadf272", size = 5233795, upload-time = "2025-09-22T04:03:27.62Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/56/4d/4856e897df0d588789dd844dbed9d91782c4ef0b327f96ce53c807e13128/lxml-6.0.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:80dadc234ebc532e09be1975ff538d154a7fa61ea5031c03d25178855544728f", size = 5257023, upload-time = "2025-09-22T04:03:30.056Z" },
|
||||||
|
{ 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 = "pygea"
|
||||||
|
version = "0.1.0"
|
||||||
|
source = { editable = "." }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "beautifulsoup4" },
|
||||||
|
{ name = "feedgen" },
|
||||||
|
{ name = "python-dateutil" },
|
||||||
|
{ name = "requests" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.metadata]
|
||||||
|
requires-dist = [
|
||||||
|
{ name = "beautifulsoup4" },
|
||||||
|
{ name = "feedgen" },
|
||||||
|
{ name = "python-dateutil" },
|
||||||
|
{ name = "requests" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "python-dateutil"
|
||||||
|
version = "2.9.0.post0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "six" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "requests"
|
||||||
|
version = "2.32.5"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "certifi" },
|
||||||
|
{ name = "charset-normalizer" },
|
||||||
|
{ name = "idna" },
|
||||||
|
{ name = "urllib3" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[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 = "soupsieve"
|
||||||
|
version = "2.8.3"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/7b/ae/2d9c981590ed9999a0d91755b47fc74f74de286b0f5cee14c9269041e6c4/soupsieve-2.8.3.tar.gz", hash = "sha256:3267f1eeea4251fb42728b6dfb746edc9acaffc4a45b27e19450b676586e8349", size = 118627, upload-time = "2026-01-20T04:27:02.457Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/46/2c/1462b1d0a634697ae9e55b3cecdcb64788e8b7d63f54d923fcd0bb140aed/soupsieve-2.8.3-py3-none-any.whl", hash = "sha256:ed64f2ba4eebeab06cc4962affce381647455978ffc1e36bb79a545b91f45a95", size = 37016, upload-time = "2026-01-20T04:27:01.012Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "typing-extensions"
|
||||||
|
version = "4.15.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "urllib3"
|
||||||
|
version = "2.6.3"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556, upload-time = "2026-01-07T16:24:43.925Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" },
|
||||||
|
]
|
||||||
Loading…
Add table
Add a link
Reference in a new issue