├── .github └── workflows │ └── listmonk_rss.yml ├── .gitignore ├── Makefile ├── README.md ├── assets └── C4 │ ├── architecture.png │ └── architecture.puml ├── attachments ├── 2025-02-07_18-25-19.png └── 2025-02-07_18-26-12.png ├── listmonk_rss.py ├── pyproject.toml ├── template.md.j2 └── uv.lock /.github/workflows/listmonk_rss.yml: -------------------------------------------------------------------------------- 1 | name: Listmonk RSS Campaign 2 | 3 | on: 4 | schedule: 5 | - cron: '00 8 * * 4' # Run every Thursday at 8:00 AM UTC 6 | workflow_dispatch: 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - name: Checkout code 14 | uses: actions/checkout@v4 15 | 16 | - name: Install uv and enable caching 17 | uses: astral-sh/setup-uv@v3 18 | with: 19 | enable-cache: true 20 | cache-dependency-glob: "uv.lock" 21 | 22 | - name: Set up Python 23 | run: uv python install 24 | 25 | - name: 👷 Run RSS campaign 26 | run: uv run python listmonk_rss.py 27 | env: 28 | RSS_FEED: ${{ vars.RSS_FEED }} 29 | LISTMONK_API_USER: ${{ vars.LISTMONK_API_USER }} 30 | LISTMONK_API_TOKEN: ${{ secrets.LISTMONK_API_TOKEN }} 31 | PUSHOVER_USER_KEY: ${{ secrets.PUSHOVER_USER_KEY }} 32 | PUSHOVER_API_TOKEN: ${{ secrets.PUSHOVER_API_TOKEN }} 33 | LISTMONK_HOST: ${{ vars.LISTMONK_HOST }} 34 | LIST_NAME: ${{ vars.LIST_NAME }} 35 | DELAY_SEND_MINS: ${{ vars.DELAY_SEND_MINS }} 36 | GH_REPOSITORY: ${{ vars.GH_REPOSITORY }} 37 | GH_TOKEN: ${{ secrets.GH_TOKEN }} 38 | 39 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Python-generated files 2 | __pycache__/ 3 | *.py[oc] 4 | build/ 5 | dist/ 6 | wheels/ 7 | *.egg-info 8 | .DS_Store 9 | 10 | # Virtual environments 11 | .venv 12 | .aider* 13 | .env 14 | last_update.json 15 | /.python-version 16 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | all: help 2 | 3 | help: ## output help for all targets 4 | @echo "Local run for creating campaigns using Listmonk" 5 | @echo "see README.md for details" 6 | @echo 7 | @awk 'BEGIN {FS = ":.*?## "}; \ 8 | /^###/ {printf "\n\033[1;33m%s\033[0m\n", substr($$0, 5)}; \ 9 | /^[a-zA-Z_-]+:.*?## / {printf "\033[36m%-18s\033[0m %s\n", $$1, $$2}' \ 10 | $(MAKEFILE_LIST) 11 | 12 | PLANTUML_DIAGRAMS=$(shell echo assets/C4/*.puml) 13 | PLANTUML_DIAGRAMS_PNG=$(PLANTUML_DIAGRAMS:.puml=.png) 14 | 15 | $(PLANTUML_DIAGRAMS_PNG): %.png: %.puml 16 | plantuml $< 17 | 18 | create_campaign: ## check for new items in your feed and create a campaign 19 | uv run listmonk_rss.py 20 | 21 | dry_run: ## check for new items in your feed and create a campaign 22 | uv run listmonk_rss.py --dry-run 23 | 24 | github_workflow: ## test github workflow using act 25 | act workflow_dispatch --secret-file .env --var-file .env --container-architecture linux/amd64 --artifact-server-path /tmp/artifacts 26 | 27 | 28 | diagrams: $(PLANTUML_DIAGRAMS_PNG) ## Generate architecture diagrams 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Listmonk RSS Newsletter Automation 2 | 3 | Automatically send newsletters from RSS feeds using [Listmonk (open 4 | source)](https://listmonk.app) and GitHub Actions, saving money compared to 5 | [Mailchimp](https://mailchimp.com/features/rss-to-email/) and other newsletter 6 | providers. 7 | 8 | ## Features 9 | 10 | - Schedule newsletter campaigns with latest update from your feed 11 | - Automatically fetch new items from RSS feeds 12 | - Create newsletter content based on a Markdown template for RSS feed items 13 | - Receive push notifications when campaign is scheduled, enough time to 14 | edit the draft 15 | - GitHub Actions integration for automated scheduling without the need for 16 | running a server 17 | - Dry run mode to test campaign creation without scheduling or updating state 18 | 19 | ## Requirements 20 | 21 | - A running Listmonk instance (e.g., deployed with 22 | [PikaPods](https://www.pikapods.com)) 23 | - GitHub account and a version of this repository 24 | - An existing RSS feed URL, obviously 25 | 26 | ## Diagram 27 | 28 | ![Architecture](assets/C4/architecture.png) 29 | 30 | ## Setup 31 | 32 | ### 1. Testing Setup 33 | 34 | 1. Fork this repository so that you can set up your own schedule and clone it. 35 | 36 | 2. On GitHub, Create a new repository variable `LAST_UPDATE` in your GitHub 37 | repo to persist the last update timestamp. 38 | 39 | 3. Create a GitHub Personal Access Token with `repo` scope at 40 | , make sure that you have the following 41 | permission set: *"Variables" repository permissions (write)* (we need to 42 | send an [API call to update a repository 43 | variable](https://docs.github.com/en/rest/actions/variables?apiVersion=2022-11-28#update-a-repository-variable)). 44 | 45 | 4. On your machine, install dependencies using uv: 46 | ```bash 47 | uv sync -U 48 | ``` 49 | 50 | 5. Create a `.env` file with your configuration: 51 | ```bash 52 | LISTMONK_HOST= 53 | LIST_NAME= 54 | LISTMONK_API_USER= 55 | LISTMONK_API_TOKEN= 56 | 57 | GH_TOKEN= 58 | GH_REPOSITORY=/listmonk-rss 59 | 60 | RSS_FEED= 61 | DELAY_SEND_MINS=45 62 | ``` 63 | 64 | 6. Test the script locally with the environment variable below: 65 | ```bash 66 | make create_campaign 67 | ``` 68 | 69 | ### Dry Run Mode 70 | 71 | To test the script without actually scheduling a campaign or updating the last update timestamp: 72 | 73 | ```bash 74 | make dry_run 75 | ``` 76 | 77 | This will: 78 | - Create a campaign draft scheduled 10 years in the future 79 | - Not update the LAST_UPDATE timestamp 80 | - Allow you to review the campaign content in Listmonk 81 | 82 | ### 2. Pushover Notifications (Optional) 83 | 84 | To receive notifications when newsletters are scheduled (this gives you an 85 | opportunity to review the content before it is sent out): 86 | 87 | 1. Create a Pushover account at https://pushover.net 88 | 2. Install the Pushover app on your devices 89 | 3. Get your User Key from the Pushover dashboard 90 | 4. Create an Application/API Token 91 | 92 | Set the env variables: 93 | 94 | ```bash 95 | PUSHOVER_USER_KEY= 96 | PUSHOVER_API_TOKEN= 97 | ``` 98 | 99 | ### 3. GitHub Actions Setup 100 | 101 | Once you have setup and tested everything locally, you can move it to GitHub: 102 | 103 | 1. Add your `.env` file contents, Pushover credentials, and GitHub token as 104 | GitHub Secrets and Environment variables in your repository (see screenshots 105 | below): 106 | - Go to Settings → Secrets and variables → Actions 107 | - There is a tab "Secrets" and a tab "Variables". 108 | 109 | 2. The workflow is already configured in `.github/workflows/listmonk_rss.yml` 110 | and does the following: 111 | - Runs on Weekdays at 8:00 UTC, change the cron schedule as you want. 112 | - Persists state between runs using the repository variable (`LAST_UPDATE`), 113 | Uses GitHub API to store and retrieve the last processed timestamp 114 | - Automatically creates and schedules newsletters based on the Python 115 | script. 116 | 117 | 3. To manually trigger the workflow for testing: 118 | - Go to Actions → Listmonk RSS 119 | - Click "Run workflow" 120 | - Check in Listmonk whether the draft campaign has been created 121 | 122 | 123 |
124 | 125 | 126 | #### Screenshot "Repository Secrets" 127 | 128 | 129 | 130 | ![](attachments/2025-02-07_18-25-19.png) 131 | 132 |
133 | 134 |
135 | 136 | 137 | #### Screenshot "Repository Variables" 138 | 139 | 140 | 141 | ![](attachments/2025-02-07_18-26-12.png) 142 | 143 |
144 | 145 | ## Configuration 146 | 147 | ### Environment Variables 148 | 149 | | Variable | Description | Required | 150 | |-----------------------|--------------------------------------------------|----------| 151 | | LISTMONK_API_USER | Listmonk API username | Yes | 152 | | LISTMONK_API_TOKEN | Listmonk API token | Yes | 153 | | LISTMONK_HOST | Listmonk instance URL | Yes | 154 | | LIST_NAME | Name of the mailing list in Listmonk | Yes | 155 | | RSS_FEED | URL of the RSS feed to monitor | Yes | 156 | | DELAY_SEND_MINS | Minutes to delay sending after creation (default: 30). In dry run mode, this is set to 10 years. | No | 157 | | PUSHOVER_USER_KEY | Pushover user key for notifications (optional) | No | 158 | | PUSHOVER_API_TOKEN | Pushover API token for notifications (optional) | No | 159 | | GH_REPOSITORY | GitHub repository in "owner/repo" format | Yes | 160 | | GH_TOKEN | GitHub token with repo scope for state storage | Yes | 161 | 162 | ### Template Customization 163 | 164 | Edit `template.md.j2` to customize your newsletter format. The template uses Jinja2 syntax and has access to: 165 | 166 | - `items`: List of RSS feed items with: 167 | - `title`: Article title 168 | - `link`: Article URL 169 | - `summary`: Article summary 170 | - `media_content`: OpenGraph image URL 171 | 172 | 173 | ## Related work and Contributing 174 | 175 | This repo is inspired by 176 | [rss2newsletter](https://github.com/ElliotKillick/rss2newsletter), but I wanted 177 | a solution that runs without setting up a dedicated server, it just needs the 178 | Listmonk instance and GitHub. 179 | 180 | Contributions are welcome, but there's no guarantee that I will have the 181 | resources to act on them. I use this repo mostly for my own purposes. My advice 182 | would be to fork it and adjust it to your needs. 183 | 184 | ## Contact 185 | 186 | You may want to subscribe to [my blog](https://blog.heuel.org) 😃. 187 | -------------------------------------------------------------------------------- /assets/C4/architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ping13/listmonk-rss/56d6c5b001dcd8774ee1493cd252f0f80e3a4cdf/assets/C4/architecture.png -------------------------------------------------------------------------------- /assets/C4/architecture.puml: -------------------------------------------------------------------------------- 1 | @startuml 2 | !include https://raw.githubusercontent.com/plantuml-stdlib/C4-PlantUML/master/C4_Container.puml 3 | 4 | ' Adjust layout to be more left-to-right 5 | left to right direction 6 | 7 | 8 | title Listmonk RSS Newsletter Automation 9 | 10 | ' Define actors and systems 11 | Person(author, "Author", "Writes blog content") 12 | Person(subscriber, "Newsletter Subscriber", "Receives newsletters") 13 | 14 | ' Automation system boundary 15 | System_Boundary(system, "Newsletter Automation", "Automated newsletter workflow") { 16 | Container(script, "Python Script", "python", "RSS feed processing") 17 | System(github_actions, "GitHub Actions", "Workflow automation") 18 | System(pushover, "Pushover", "Notification service") 19 | } 20 | 21 | 22 | ' Content Management Systems boundary 23 | System(listmonk, "Listmonk", "Newsletter management: subscribers, lists, campaigns") 24 | System(blog, "Blog with RSS Feed", "Content source") 25 | 26 | 27 | ' Relationships 28 | Rel_Neighbor(author, blog, "writes content") 29 | Rel_Neighbor(subscriber, blog, "reads blog and subscribes to", "Web") 30 | Rel(blog,listmonk, "signup form for a newsletter subscription", "HTML") 31 | Rel(blog, script, "provides RSS feed") 32 | Rel(script, listmonk, "creates a scheduled campaigns", "HTTP API") 33 | Rel(script, pushover, "sends notifications when campaign is scheduled", "HTTP API") 34 | Rel(pushover, author, "notifies about a scheduled campaign", "Pushover") 35 | Rel(listmonk, subscriber, "sends newsletters", "SMTP") 36 | Rel(github_actions, script, "triggers", "cron-job") 37 | 38 | SHOW_FLOATING_LEGEND() 39 | 40 | @enduml 41 | -------------------------------------------------------------------------------- /attachments/2025-02-07_18-25-19.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ping13/listmonk-rss/56d6c5b001dcd8774ee1493cd252f0f80e3a4cdf/attachments/2025-02-07_18-25-19.png -------------------------------------------------------------------------------- /attachments/2025-02-07_18-26-12.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ping13/listmonk-rss/56d6c5b001dcd8774ee1493cd252f0f80e3a4cdf/attachments/2025-02-07_18-26-12.png -------------------------------------------------------------------------------- /listmonk_rss.py: -------------------------------------------------------------------------------- 1 | import os 2 | import json 3 | from datetime import datetime, timedelta, timezone 4 | from pathlib import Path 5 | 6 | import httpx 7 | import feedparser 8 | from jinja2 import Template 9 | from dotenv import load_dotenv 10 | from bs4 import BeautifulSoup 11 | import click 12 | import logging 13 | 14 | logging.basicConfig(level=logging.INFO) # Set to DEBUG, INFO, WARNING, ERROR, or CRITICAL 15 | 16 | # Load environment variables 17 | load_dotenv() 18 | 19 | # Constants 20 | TEMPLATE_FILE = Path("template.md.j2") 21 | 22 | def get_opengraph_data(url): 23 | response = httpx.get(url) 24 | soup = BeautifulSoup(response.text, 'html.parser') 25 | og_data = {} 26 | for meta in soup.find_all('meta'): 27 | prop = meta.get('property', '') 28 | if prop.startswith('og:'): 29 | key = prop[3:] 30 | og_data[key] = meta.get('content', '') 31 | return og_data 32 | 33 | 34 | def get_last_update() -> datetime: 35 | """Get the last update timestamp from GitHub repo variable.""" 36 | github_token = os.getenv("GH_TOKEN") 37 | repo = os.getenv("GH_REPOSITORY") 38 | url = f"https://api.github.com/repos/{repo}/actions/variables/LAST_UPDATE" 39 | 40 | headers = { 41 | "Accept": "application/vnd.github+json", 42 | "Authorization": f"Bearer {github_token}", 43 | "X-GitHub-Api-Version": "2022-11-28" 44 | } 45 | 46 | try: 47 | response = httpx.get(url, headers=headers) 48 | response.raise_for_status() 49 | data = response.json() 50 | return datetime.fromisoformat(data["value"]) 51 | except httpx.HTTPStatusError as e: 52 | if e.response.status_code == 404: 53 | return datetime.min 54 | raise 55 | 56 | 57 | def save_last_update(timestamp: datetime): 58 | """Save the last update timestamp to GitHub repo variable.""" 59 | github_token = os.getenv("GH_TOKEN") 60 | repo = os.getenv("GH_REPOSITORY") 61 | url = f"https://api.github.com/repos/{repo}/actions/variables/LAST_UPDATE" 62 | 63 | headers = { 64 | "Accept": "application/vnd.github+json", 65 | "Authorization": f"Bearer {github_token}", 66 | "X-GitHub-Api-Version": "2022-11-28" 67 | } 68 | 69 | data = { 70 | "name": "LAST_UPDATE", 71 | "value": timestamp.isoformat() 72 | } 73 | 74 | response = httpx.patch(url, headers=headers, json=data) 75 | response.raise_for_status() 76 | logging.info(f"Saved last update timestamp to GitHub repo variable") 77 | 78 | 79 | def fetch_rss_feed(feed_url: str, last_update: datetime) -> list: 80 | """Fetch and parse RSS feed, returning new items since last update.""" 81 | feed = feedparser.parse(feed_url) 82 | new_items = [] 83 | logging.info(f"There are in total {len(feed.entries)} entries for {feed_url}") 84 | for entry in feed.entries: 85 | if datetime(*entry.published_parsed[:6]) > last_update: 86 | og = get_opengraph_data(entry.link) 87 | if og.get("image"): 88 | entry.media_content=og.get("image") 89 | new_items.append(entry) 90 | 91 | return new_items 92 | 93 | 94 | def get_list_id(host: str, api_user: str, api_token: str, list_name: str) -> int: 95 | """Get list ID from list name using Listmonk API.""" 96 | url = f"{host}/api/lists" 97 | auth=(api_user, api_token) 98 | headers = { 99 | "Content-Type": "application/json" 100 | } 101 | 102 | with httpx.Client() as client: 103 | response = client.get(url, headers=headers, auth=auth ) 104 | response.raise_for_status() 105 | 106 | # Find the list with matching name 107 | lists = response.json()["data"]["results"] 108 | for lst in lists: 109 | if lst["name"] == list_name: 110 | return lst["id"] 111 | 112 | raise ValueError(f"List '{list_name}' not found") 113 | 114 | 115 | def create_campaign_content(items: list, template: Template) -> str: 116 | """Generate campaign content using Jinja2 template.""" 117 | return template.render(items=items) 118 | 119 | 120 | def schedule_campaign(host: str, api_user: str, api_token: str, list_id: int, content: str, subject: str, dry_run: bool = False): 121 | """Send campaign draft using Listmonk API.""" 122 | url = f"{host}/api/campaigns" 123 | auth=(api_user, api_token) 124 | headers = { 125 | "Content-Type": "application/json" 126 | } 127 | current_datetime = datetime.now().strftime("%Y-%m-%d %H:%M") 128 | 129 | # for the send time, we assume that the linkmonk server runs in UTC (this 130 | # is what the default in PikaPods are) 131 | if dry_run: 132 | # Set delay to 10 years for dry run 133 | print("*** This is a dry run, the campaign is scheduled 10 years in the future") 134 | delay_mins = 365 * 24 * 60 * 10 135 | else: 136 | delay_mins = int(os.getenv("DELAY_SEND_MINS", 30)) 137 | 138 | send_datetime = datetime.now(timezone.utc) + timedelta(minutes=delay_mins) 139 | send_datetime = send_datetime.strftime("%Y-%m-%dT%H:%M:%SZ") 140 | data = { 141 | "name" : f"RSS Update Newsletter, {current_datetime}", 142 | "subject": subject, 143 | "lists": [list_id], 144 | "body": content, 145 | "content_type": "markdown", 146 | "type": "regular", 147 | "send_at" : send_datetime 148 | } 149 | 150 | with httpx.Client() as client: 151 | response = client.post(url, json=data, headers=headers,auth=auth) 152 | response.raise_for_status() 153 | 154 | parsed = response.json() 155 | assert parsed.get("data",{}).get("id",None), "Cannot get the id of the created campaign" 156 | campaign_id = parsed.get("data",{}).get("id") 157 | 158 | print(f"Campaign draft {campaign_id} successfully created!") 159 | 160 | 161 | url = f"{url}/{campaign_id}/status" 162 | data = {"status": "scheduled"} 163 | headers = { 164 | "Content-Type": "application/json" 165 | } 166 | 167 | with httpx.Client() as client: 168 | response = client.put(url, json=data, auth=auth) 169 | response.raise_for_status() 170 | 171 | assert parsed.get("data",{}).get("id",None) == campaign_id, f"Cannot schedule campaign {campaign_id}" 172 | 173 | print(f"Campaign {campaign_id} successfully scheduled with {delay_mins} mins delay!") 174 | 175 | # Send Pushover notification 176 | pushover_user_key = os.getenv("PUSHOVER_USER_KEY") 177 | pushover_api_token = os.getenv("PUSHOVER_API_TOKEN") 178 | 179 | if pushover_user_key and pushover_api_token: 180 | response = httpx.post( 181 | "https://api.pushover.net/1/messages.json", 182 | data={ 183 | "token": pushover_api_token, 184 | "user": pushover_user_key, 185 | "message": f"A new campaign has been successfully scheduled with {delay_mins} mins delay! Check if you want to review this before sending.", 186 | "title": "Newsletter for your blog" 187 | }, 188 | headers={"Content-type": "application/x-www-form-urlencoded"} 189 | ) 190 | response.raise_for_status() 191 | 192 | return True 193 | 194 | @click.command() 195 | @click.option("--dry-run", is_flag=True, help="Create draft campaign with a 10-year delay and don't update last update time.") 196 | def main(dry_run: bool): 197 | if dry_run: 198 | print("*** This is a dry run") 199 | 200 | 201 | assert os.getenv("RSS_FEED"), "No RSS feed given" 202 | # Load template 203 | template = Template(TEMPLATE_FILE.read_text()) 204 | 205 | # Get last update time 206 | last_update = get_last_update() 207 | 208 | # Fetch new RSS items 209 | items = list(reversed(fetch_rss_feed(os.getenv("RSS_FEED"), last_update))) 210 | 211 | if not items: 212 | print(f"No new items found, I keep the update as of my last state '{last_update}' (UTC) in GitHub.") 213 | return 214 | 215 | # Create campaign content 216 | content = create_campaign_content(items, template) 217 | 218 | # Get list ID 219 | list_id = get_list_id( 220 | host=os.getenv("LISTMONK_HOST"), 221 | api_user=os.getenv("LISTMONK_API_USER"), 222 | api_token=os.getenv("LISTMONK_API_TOKEN"), 223 | list_name=os.getenv("LIST_NAME") 224 | ) 225 | 226 | # Schedule campaign 227 | success = schedule_campaign( 228 | host=os.getenv("LISTMONK_HOST"), 229 | api_user=os.getenv("LISTMONK_API_USER"), 230 | api_token=os.getenv("LISTMONK_API_TOKEN"), 231 | list_id=list_id, 232 | content=content, 233 | subject=os.getenv("LIST_NAME"), 234 | dry_run=dry_run 235 | ) 236 | 237 | # Update last update time only if not dry run and successful 238 | if success and not dry_run: 239 | save_last_update(datetime.now()) 240 | elif dry_run: 241 | print("*** This is a dry run, I don't update the last_save state") 242 | elif not success: 243 | print("*** Something went wrong with scheduling the campaign") 244 | 245 | 246 | if __name__ == "__main__": 247 | main() 248 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "listmonk-rss" 3 | version = "0.1.0" 4 | description = "Add your description here" 5 | readme = "README.md" 6 | requires-python = ">=3.12" 7 | dependencies = [ 8 | "beautifulsoup4>=4.13.3", 9 | "click>=8.1.8", 10 | "feedparser>=6.0.11", 11 | "httpx>=0.28.1", 12 | "jinja2>=3.1.5", 13 | "python-dotenv>=1.0.1", 14 | ] 15 | -------------------------------------------------------------------------------- /template.md.j2: -------------------------------------------------------------------------------- 1 | Hi there, 2 | 3 | Thank you for your interest in my blog! Here are the latest updates. 4 | 5 | Cheers 6 | Stephan 7 | 8 | {% for item in items %} 9 | ## [{{ item.title }}]({{ item.link }}) 10 | 11 | {{ item.summary|safe }} 12 | 13 | ![Image]({{ item.media_content }}) 14 | 15 | {% endfor %} 16 | -------------------------------------------------------------------------------- /uv.lock: -------------------------------------------------------------------------------- 1 | version = 1 2 | revision = 1 3 | requires-python = ">=3.12" 4 | 5 | [[package]] 6 | name = "anyio" 7 | version = "4.9.0" 8 | source = { registry = "https://pypi.org/simple" } 9 | dependencies = [ 10 | { name = "idna" }, 11 | { name = "sniffio" }, 12 | { name = "typing-extensions", marker = "python_full_version < '3.13'" }, 13 | ] 14 | sdist = { url = "https://files.pythonhosted.org/packages/95/7d/4c1bd541d4dffa1b52bd83fb8527089e097a106fc90b467a7313b105f840/anyio-4.9.0.tar.gz", hash = "sha256:673c0c244e15788651a4ff38710fea9675823028a6f08a5eda409e0c9840a028", size = 190949 } 15 | wheels = [ 16 | { url = "https://files.pythonhosted.org/packages/a1/ee/48ca1a7c89ffec8b6a0c5d02b89c305671d5ffd8d3c94acf8b8c408575bb/anyio-4.9.0-py3-none-any.whl", hash = "sha256:9f76d541cad6e36af7beb62e978876f3b41e3e04f2c1fbf0884604c0a9c4d93c", size = 100916 }, 17 | ] 18 | 19 | [[package]] 20 | name = "beautifulsoup4" 21 | version = "4.13.4" 22 | source = { registry = "https://pypi.org/simple" } 23 | dependencies = [ 24 | { name = "soupsieve" }, 25 | { name = "typing-extensions" }, 26 | ] 27 | sdist = { url = "https://files.pythonhosted.org/packages/d8/e4/0c4c39e18fd76d6a628d4dd8da40543d136ce2d1752bd6eeeab0791f4d6b/beautifulsoup4-4.13.4.tar.gz", hash = "sha256:dbb3c4e1ceae6aefebdaf2423247260cd062430a410e38c66f2baa50a8437195", size = 621067 } 28 | wheels = [ 29 | { url = "https://files.pythonhosted.org/packages/50/cd/30110dc0ffcf3b131156077b90e9f60ed75711223f306da4db08eff8403b/beautifulsoup4-4.13.4-py3-none-any.whl", hash = "sha256:9bbbb14bfde9d79f38b8cd5f8c7c85f4b8f2523190ebed90e950a8dea4cb1c4b", size = 187285 }, 30 | ] 31 | 32 | [[package]] 33 | name = "certifi" 34 | version = "2025.4.26" 35 | source = { registry = "https://pypi.org/simple" } 36 | sdist = { url = "https://files.pythonhosted.org/packages/e8/9e/c05b3920a3b7d20d3d3310465f50348e5b3694f4f88c6daf736eef3024c4/certifi-2025.4.26.tar.gz", hash = "sha256:0a816057ea3cdefcef70270d2c515e4506bbc954f417fa5ade2021213bb8f0c6", size = 160705 } 37 | wheels = [ 38 | { url = "https://files.pythonhosted.org/packages/4a/7e/3db2bd1b1f9e95f7cddca6d6e75e2f2bd9f51b1246e546d88addca0106bd/certifi-2025.4.26-py3-none-any.whl", hash = "sha256:30350364dfe371162649852c63336a15c70c6510c2ad5015b21c2345311805f3", size = 159618 }, 39 | ] 40 | 41 | [[package]] 42 | name = "click" 43 | version = "8.2.1" 44 | source = { registry = "https://pypi.org/simple" } 45 | dependencies = [ 46 | { name = "colorama", marker = "sys_platform == 'win32'" }, 47 | ] 48 | sdist = { url = "https://files.pythonhosted.org/packages/60/6c/8ca2efa64cf75a977a0d7fac081354553ebe483345c734fb6b6515d96bbc/click-8.2.1.tar.gz", hash = "sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202", size = 286342 } 49 | wheels = [ 50 | { url = "https://files.pythonhosted.org/packages/85/32/10bb5764d90a8eee674e9dc6f4db6a0ab47c8c4d0d83c27f7c39ac415a4d/click-8.2.1-py3-none-any.whl", hash = "sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b", size = 102215 }, 51 | ] 52 | 53 | [[package]] 54 | name = "colorama" 55 | version = "0.4.6" 56 | source = { registry = "https://pypi.org/simple" } 57 | sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 } 58 | wheels = [ 59 | { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, 60 | ] 61 | 62 | [[package]] 63 | name = "feedparser" 64 | version = "6.0.11" 65 | source = { registry = "https://pypi.org/simple" } 66 | dependencies = [ 67 | { name = "sgmllib3k" }, 68 | ] 69 | sdist = { url = "https://files.pythonhosted.org/packages/ff/aa/7af346ebeb42a76bf108027fe7f3328bb4e57a3a96e53e21fd9ef9dd6dd0/feedparser-6.0.11.tar.gz", hash = "sha256:c9d0407b64c6f2a065d0ebb292c2b35c01050cc0dc33757461aaabdc4c4184d5", size = 286197 } 70 | wheels = [ 71 | { url = "https://files.pythonhosted.org/packages/7c/d4/8c31aad9cc18f451c49f7f9cfb5799dadffc88177f7917bc90a66459b1d7/feedparser-6.0.11-py3-none-any.whl", hash = "sha256:0be7ee7b395572b19ebeb1d6aafb0028dee11169f1c934e0ed67d54992f4ad45", size = 81343 }, 72 | ] 73 | 74 | [[package]] 75 | name = "h11" 76 | version = "0.16.0" 77 | source = { registry = "https://pypi.org/simple" } 78 | sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250 } 79 | wheels = [ 80 | { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515 }, 81 | ] 82 | 83 | [[package]] 84 | name = "httpcore" 85 | version = "1.0.9" 86 | source = { registry = "https://pypi.org/simple" } 87 | dependencies = [ 88 | { name = "certifi" }, 89 | { name = "h11" }, 90 | ] 91 | sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484 } 92 | wheels = [ 93 | { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784 }, 94 | ] 95 | 96 | [[package]] 97 | name = "httpx" 98 | version = "0.28.1" 99 | source = { registry = "https://pypi.org/simple" } 100 | dependencies = [ 101 | { name = "anyio" }, 102 | { name = "certifi" }, 103 | { name = "httpcore" }, 104 | { name = "idna" }, 105 | ] 106 | sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406 } 107 | wheels = [ 108 | { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517 }, 109 | ] 110 | 111 | [[package]] 112 | name = "idna" 113 | version = "3.10" 114 | source = { registry = "https://pypi.org/simple" } 115 | sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490 } 116 | wheels = [ 117 | { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 }, 118 | ] 119 | 120 | [[package]] 121 | name = "jinja2" 122 | version = "3.1.6" 123 | source = { registry = "https://pypi.org/simple" } 124 | dependencies = [ 125 | { name = "markupsafe" }, 126 | ] 127 | sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115 } 128 | wheels = [ 129 | { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899 }, 130 | ] 131 | 132 | [[package]] 133 | name = "listmonk-rss" 134 | version = "0.1.0" 135 | source = { virtual = "." } 136 | dependencies = [ 137 | { name = "beautifulsoup4" }, 138 | { name = "click" }, 139 | { name = "feedparser" }, 140 | { name = "httpx" }, 141 | { name = "jinja2" }, 142 | { name = "python-dotenv" }, 143 | ] 144 | 145 | [package.metadata] 146 | requires-dist = [ 147 | { name = "beautifulsoup4", specifier = ">=4.13.3" }, 148 | { name = "click", specifier = ">=8.1.8" }, 149 | { name = "feedparser", specifier = ">=6.0.11" }, 150 | { name = "httpx", specifier = ">=0.28.1" }, 151 | { name = "jinja2", specifier = ">=3.1.5" }, 152 | { name = "python-dotenv", specifier = ">=1.0.1" }, 153 | ] 154 | 155 | [[package]] 156 | name = "markupsafe" 157 | version = "3.0.2" 158 | source = { registry = "https://pypi.org/simple" } 159 | sdist = { url = "https://files.pythonhosted.org/packages/b2/97/5d42485e71dfc078108a86d6de8fa46db44a1a9295e89c5d6d4a06e23a62/markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0", size = 20537 } 160 | wheels = [ 161 | { url = "https://files.pythonhosted.org/packages/22/09/d1f21434c97fc42f09d290cbb6350d44eb12f09cc62c9476effdb33a18aa/MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf", size = 14274 }, 162 | { url = "https://files.pythonhosted.org/packages/6b/b0/18f76bba336fa5aecf79d45dcd6c806c280ec44538b3c13671d49099fdd0/MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225", size = 12348 }, 163 | { url = "https://files.pythonhosted.org/packages/e0/25/dd5c0f6ac1311e9b40f4af06c78efde0f3b5cbf02502f8ef9501294c425b/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028", size = 24149 }, 164 | { url = "https://files.pythonhosted.org/packages/f3/f0/89e7aadfb3749d0f52234a0c8c7867877876e0a20b60e2188e9850794c17/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8", size = 23118 }, 165 | { url = "https://files.pythonhosted.org/packages/d5/da/f2eeb64c723f5e3777bc081da884b414671982008c47dcc1873d81f625b6/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c", size = 22993 }, 166 | { url = "https://files.pythonhosted.org/packages/da/0e/1f32af846df486dce7c227fe0f2398dc7e2e51d4a370508281f3c1c5cddc/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557", size = 24178 }, 167 | { url = "https://files.pythonhosted.org/packages/c4/f6/bb3ca0532de8086cbff5f06d137064c8410d10779c4c127e0e47d17c0b71/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22", size = 23319 }, 168 | { url = "https://files.pythonhosted.org/packages/a2/82/8be4c96ffee03c5b4a034e60a31294daf481e12c7c43ab8e34a1453ee48b/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48", size = 23352 }, 169 | { url = "https://files.pythonhosted.org/packages/51/ae/97827349d3fcffee7e184bdf7f41cd6b88d9919c80f0263ba7acd1bbcb18/MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30", size = 15097 }, 170 | { url = "https://files.pythonhosted.org/packages/c1/80/a61f99dc3a936413c3ee4e1eecac96c0da5ed07ad56fd975f1a9da5bc630/MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87", size = 15601 }, 171 | { url = "https://files.pythonhosted.org/packages/83/0e/67eb10a7ecc77a0c2bbe2b0235765b98d164d81600746914bebada795e97/MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd", size = 14274 }, 172 | { url = "https://files.pythonhosted.org/packages/2b/6d/9409f3684d3335375d04e5f05744dfe7e9f120062c9857df4ab490a1031a/MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430", size = 12352 }, 173 | { url = "https://files.pythonhosted.org/packages/d2/f5/6eadfcd3885ea85fe2a7c128315cc1bb7241e1987443d78c8fe712d03091/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094", size = 24122 }, 174 | { url = "https://files.pythonhosted.org/packages/0c/91/96cf928db8236f1bfab6ce15ad070dfdd02ed88261c2afafd4b43575e9e9/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396", size = 23085 }, 175 | { url = "https://files.pythonhosted.org/packages/c2/cf/c9d56af24d56ea04daae7ac0940232d31d5a8354f2b457c6d856b2057d69/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79", size = 22978 }, 176 | { url = "https://files.pythonhosted.org/packages/2a/9f/8619835cd6a711d6272d62abb78c033bda638fdc54c4e7f4272cf1c0962b/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a", size = 24208 }, 177 | { url = "https://files.pythonhosted.org/packages/f9/bf/176950a1792b2cd2102b8ffeb5133e1ed984547b75db47c25a67d3359f77/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca", size = 23357 }, 178 | { url = "https://files.pythonhosted.org/packages/ce/4f/9a02c1d335caabe5c4efb90e1b6e8ee944aa245c1aaaab8e8a618987d816/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c", size = 23344 }, 179 | { url = "https://files.pythonhosted.org/packages/ee/55/c271b57db36f748f0e04a759ace9f8f759ccf22b4960c270c78a394f58be/MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1", size = 15101 }, 180 | { url = "https://files.pythonhosted.org/packages/29/88/07df22d2dd4df40aba9f3e402e6dc1b8ee86297dddbad4872bd5e7b0094f/MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f", size = 15603 }, 181 | { url = "https://files.pythonhosted.org/packages/62/6a/8b89d24db2d32d433dffcd6a8779159da109842434f1dd2f6e71f32f738c/MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c", size = 14510 }, 182 | { url = "https://files.pythonhosted.org/packages/7a/06/a10f955f70a2e5a9bf78d11a161029d278eeacbd35ef806c3fd17b13060d/MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb", size = 12486 }, 183 | { url = "https://files.pythonhosted.org/packages/34/cf/65d4a571869a1a9078198ca28f39fba5fbb910f952f9dbc5220afff9f5e6/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c", size = 25480 }, 184 | { url = "https://files.pythonhosted.org/packages/0c/e3/90e9651924c430b885468b56b3d597cabf6d72be4b24a0acd1fa0e12af67/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d", size = 23914 }, 185 | { url = "https://files.pythonhosted.org/packages/66/8c/6c7cf61f95d63bb866db39085150df1f2a5bd3335298f14a66b48e92659c/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe", size = 23796 }, 186 | { url = "https://files.pythonhosted.org/packages/bb/35/cbe9238ec3f47ac9a7c8b3df7a808e7cb50fe149dc7039f5f454b3fba218/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5", size = 25473 }, 187 | { url = "https://files.pythonhosted.org/packages/e6/32/7621a4382488aa283cc05e8984a9c219abad3bca087be9ec77e89939ded9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a", size = 24114 }, 188 | { url = "https://files.pythonhosted.org/packages/0d/80/0985960e4b89922cb5a0bac0ed39c5b96cbc1a536a99f30e8c220a996ed9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9", size = 24098 }, 189 | { url = "https://files.pythonhosted.org/packages/82/78/fedb03c7d5380df2427038ec8d973587e90561b2d90cd472ce9254cf348b/MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6", size = 15208 }, 190 | { url = "https://files.pythonhosted.org/packages/4f/65/6079a46068dfceaeabb5dcad6d674f5f5c61a6fa5673746f42a9f4c233b3/MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f", size = 15739 }, 191 | ] 192 | 193 | [[package]] 194 | name = "python-dotenv" 195 | version = "1.1.0" 196 | source = { registry = "https://pypi.org/simple" } 197 | sdist = { url = "https://files.pythonhosted.org/packages/88/2c/7bb1416c5620485aa793f2de31d3df393d3686aa8a8506d11e10e13c5baf/python_dotenv-1.1.0.tar.gz", hash = "sha256:41f90bc6f5f177fb41f53e87666db362025010eb28f60a01c9143bfa33a2b2d5", size = 39920 } 198 | wheels = [ 199 | { url = "https://files.pythonhosted.org/packages/1e/18/98a99ad95133c6a6e2005fe89faedf294a748bd5dc803008059409ac9b1e/python_dotenv-1.1.0-py3-none-any.whl", hash = "sha256:d7c01d9e2293916c18baf562d95698754b0dbbb5e74d457c45d4f6561fb9d55d", size = 20256 }, 200 | ] 201 | 202 | [[package]] 203 | name = "sgmllib3k" 204 | version = "1.0.0" 205 | source = { registry = "https://pypi.org/simple" } 206 | sdist = { url = "https://files.pythonhosted.org/packages/9e/bd/3704a8c3e0942d711c1299ebf7b9091930adae6675d7c8f476a7ce48653c/sgmllib3k-1.0.0.tar.gz", hash = "sha256:7868fb1c8bfa764c1ac563d3cf369c381d1325d36124933a726f29fcdaa812e9", size = 5750 } 207 | 208 | [[package]] 209 | name = "sniffio" 210 | version = "1.3.1" 211 | source = { registry = "https://pypi.org/simple" } 212 | sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372 } 213 | wheels = [ 214 | { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235 }, 215 | ] 216 | 217 | [[package]] 218 | name = "soupsieve" 219 | version = "2.7" 220 | source = { registry = "https://pypi.org/simple" } 221 | sdist = { url = "https://files.pythonhosted.org/packages/3f/f4/4a80cd6ef364b2e8b65b15816a843c0980f7a5a2b4dc701fc574952aa19f/soupsieve-2.7.tar.gz", hash = "sha256:ad282f9b6926286d2ead4750552c8a6142bc4c783fd66b0293547c8fe6ae126a", size = 103418 } 222 | wheels = [ 223 | { url = "https://files.pythonhosted.org/packages/e7/9c/0e6afc12c269578be5c0c1c9f4b49a8d32770a080260c333ac04cc1c832d/soupsieve-2.7-py3-none-any.whl", hash = "sha256:6e60cc5c1ffaf1cebcc12e8188320b72071e922c2e897f737cadce79ad5d30c4", size = 36677 }, 224 | ] 225 | 226 | [[package]] 227 | name = "typing-extensions" 228 | version = "4.13.2" 229 | source = { registry = "https://pypi.org/simple" } 230 | sdist = { url = "https://files.pythonhosted.org/packages/f6/37/23083fcd6e35492953e8d2aaaa68b860eb422b34627b13f2ce3eb6106061/typing_extensions-4.13.2.tar.gz", hash = "sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef", size = 106967 } 231 | wheels = [ 232 | { url = "https://files.pythonhosted.org/packages/8b/54/b1ae86c0973cc6f0210b53d508ca3641fb6d0c56823f288d108bc7ab3cc8/typing_extensions-4.13.2-py3-none-any.whl", hash = "sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c", size = 45806 }, 233 | ] 234 | --------------------------------------------------------------------------------