From 804a751fdf4eb49c92d0198f02f198e0e64d846b Mon Sep 17 00:00:00 2001 From: Dongho Kim Date: Wed, 12 Nov 2025 16:26:59 +0100 Subject: [PATCH] weather --- news_crawler/ollama_client.py | 34 +++++++ news_sender/newsletter_template.html | 63 +++++++++++++ news_sender/requirements.txt | 1 + news_sender/sender_service.py | 7 +- news_sender/weather_service.py | 130 +++++++++++++++++++++++++++ 5 files changed, 234 insertions(+), 1 deletion(-) create mode 100644 news_sender/weather_service.py diff --git a/news_crawler/ollama_client.py b/news_crawler/ollama_client.py index d86672d..f8b962c 100644 --- a/news_crawler/ollama_client.py +++ b/news_crawler/ollama_client.py @@ -112,6 +112,9 @@ class OllamaClient: 'duration': time.time() - start_time } + # Clean markdown formatting from summary + summary = self._clean_markdown(summary) + summary_word_count = len(summary.split()) return { @@ -303,6 +306,35 @@ German headline: return translation + def _clean_markdown(self, text): + """Remove markdown formatting from text""" + import re + + # Remove markdown headers (##, ###, etc.) + text = re.sub(r'^#{1,6}\s+', '', text, flags=re.MULTILINE) + + # Remove bold/italic markers (**text**, *text*, __text__, _text_) + text = re.sub(r'\*\*([^\*]+)\*\*', r'\1', text) + text = re.sub(r'__([^_]+)__', r'\1', text) + text = re.sub(r'\*([^\*]+)\*', r'\1', text) + text = re.sub(r'_([^_]+)_', r'\1', text) + + # Remove markdown links [text](url) -> text + text = re.sub(r'\[([^\]]+)\]\([^\)]+\)', r'\1', text) + + # Remove inline code `text` + text = re.sub(r'`([^`]+)`', r'\1', text) + + # Remove bullet points and list markers + text = re.sub(r'^\s*[-*+]\s+', '', text, flags=re.MULTILINE) + text = re.sub(r'^\s*\d+\.\s+', '', text, flags=re.MULTILINE) + + # Clean up extra whitespace + text = re.sub(r'\n\s*\n', '\n\n', text) + text = text.strip() + + return text + def _build_summarization_prompt(self, content, max_words): """Build prompt for article summarization""" # Truncate content if too long (keep first 5000 words) @@ -319,6 +351,8 @@ Write in the clear, engaging, and authoritative style of New York Times Magazine - Focus on what matters to readers - Even if the source is in German or another language, write your summary entirely in English +IMPORTANT: Write in plain text only. Do NOT use markdown formatting (no ##, **, *, bullets, etc.). Just write natural prose. + Article: {content} diff --git a/news_sender/newsletter_template.html b/news_sender/newsletter_template.html index 4d88939..e8a72b6 100644 --- a/news_sender/newsletter_template.html +++ b/news_sender/newsletter_template.html @@ -40,6 +40,39 @@

Here's what's happening in Munich today. We've summarized {{ article_count }} stories using AI so you can stay informed in under 5 minutes.

+ + {% if weather and weather.success %} + + + + + +
+ + + + + +
+

+ TODAY'S WEATHER +

+

+ {{ weather.icon }} {{ weather.temperature }}°C +

+

+ {{ weather.condition }} +

+
+

+ High: {{ weather.high }}°C +

+

+ Low: {{ weather.low }}°C +

+
+
+ {% endif %} @@ -256,6 +289,36 @@ + + + +

+ 📋 TL;DR - Quick Summary +

+

+ Here's everything in one sentence each: +

+ + {% set all_articles = (trending_articles or []) + (other_articles or []) %} + {% for article in all_articles %} +
+ {{ loop.index }}. +

+ {{ article.title_en if article.title_en else article.title }} — + {{ article.summary.split('.')[0] }}. +

+
+ {% endfor %} + + + + + + +
+ + + diff --git a/news_sender/requirements.txt b/news_sender/requirements.txt index 14d0870..576daf3 100644 --- a/news_sender/requirements.txt +++ b/news_sender/requirements.txt @@ -4,3 +4,4 @@ Jinja2==3.1.2 beautifulsoup4==4.12.2 schedule==1.2.0 pytz==2023.3 +requests==2.31.0 diff --git a/news_sender/sender_service.py b/news_sender/sender_service.py index 8211896..46b99c4 100644 --- a/news_sender/sender_service.py +++ b/news_sender/sender_service.py @@ -242,6 +242,10 @@ def render_newsletter_html(articles, tracking_enabled=False, pixel_tracking_id=N trending_articles = articles[:3] if len(articles) >= 3 else articles other_articles = articles[3:] if len(articles) > 3 else [] + # Get weather data + from weather_service import get_munich_weather + weather = get_munich_weather() + # Prepare template data now = datetime.now() template_data = { @@ -252,7 +256,8 @@ def render_newsletter_html(articles, tracking_enabled=False, pixel_tracking_id=N 'other_articles': other_articles, 'unsubscribe_link': f'{Config.WEBSITE_URL}/unsubscribe', 'website_link': Config.WEBSITE_URL, - 'tracking_enabled': tracking_enabled + 'tracking_enabled': tracking_enabled, + 'weather': weather } # Render HTML diff --git a/news_sender/weather_service.py b/news_sender/weather_service.py new file mode 100644 index 0000000..999e087 --- /dev/null +++ b/news_sender/weather_service.py @@ -0,0 +1,130 @@ +""" +Weather service for Munich using Open-Meteo API (free, no API key needed) +""" +import requests +from datetime import datetime + + +def get_munich_weather(): + """ + Get current weather for Munich, Germany + + Returns: + { + 'success': bool, + 'temperature': float, # Celsius + 'condition': str, # e.g., "Partly cloudy" + 'icon': str, # Weather emoji + 'high': float, # Today's high + 'low': float, # Today's low + 'error': str or None + } + """ + try: + # Munich coordinates + lat = 48.1351 + lon = 11.5820 + + # Open-Meteo API (free, no key needed) + url = f"https://api.open-meteo.com/v1/forecast" + params = { + 'latitude': lat, + 'longitude': lon, + 'current': 'temperature_2m,weather_code', + 'daily': 'temperature_2m_max,temperature_2m_min', + 'timezone': 'Europe/Berlin', + 'forecast_days': 1 + } + + response = requests.get(url, params=params, timeout=10) + response.raise_for_status() + data = response.json() + + # Extract data + current = data.get('current', {}) + daily = data.get('daily', {}) + + temperature = current.get('temperature_2m') + weather_code = current.get('weather_code', 0) + high = daily.get('temperature_2m_max', [None])[0] + low = daily.get('temperature_2m_min', [None])[0] + + # Map weather code to condition and emoji + condition, icon = _get_weather_description(weather_code) + + return { + 'success': True, + 'temperature': round(temperature, 1) if temperature else None, + 'condition': condition, + 'icon': icon, + 'high': round(high, 1) if high else None, + 'low': round(low, 1) if low else None, + 'error': None + } + + except requests.exceptions.Timeout: + return { + 'success': False, + 'temperature': None, + 'condition': 'Unknown', + 'icon': '🌡️', + 'high': None, + 'low': None, + 'error': 'Weather service timeout' + } + except Exception as e: + return { + 'success': False, + 'temperature': None, + 'condition': 'Unknown', + 'icon': '🌡️', + 'high': None, + 'low': None, + 'error': str(e) + } + + +def _get_weather_description(code): + """ + Map WMO weather code to description and emoji + https://open-meteo.com/en/docs + """ + weather_map = { + 0: ('Clear sky', '☀️'), + 1: ('Mainly clear', '🌤️'), + 2: ('Partly cloudy', '⛅'), + 3: ('Overcast', '☁️'), + 45: ('Foggy', '🌫️'), + 48: ('Foggy', '🌫️'), + 51: ('Light drizzle', '🌦️'), + 53: ('Drizzle', '🌦️'), + 55: ('Heavy drizzle', '🌧️'), + 61: ('Light rain', '🌧️'), + 63: ('Rain', '🌧️'), + 65: ('Heavy rain', '⛈️'), + 71: ('Light snow', '🌨️'), + 73: ('Snow', '❄️'), + 75: ('Heavy snow', '❄️'), + 77: ('Snow grains', '🌨️'), + 80: ('Light showers', '🌦️'), + 81: ('Showers', '🌧️'), + 82: ('Heavy showers', '⛈️'), + 85: ('Light snow showers', '🌨️'), + 86: ('Snow showers', '❄️'), + 95: ('Thunderstorm', '⛈️'), + 96: ('Thunderstorm with hail', '⛈️'), + 99: ('Thunderstorm with hail', '⛈️'), + } + + return weather_map.get(code, ('Unknown', '🌡️')) + + +if __name__ == '__main__': + # Test + weather = get_munich_weather() + print(f"Success: {weather['success']}") + print(f"Temperature: {weather['temperature']}°C") + print(f"Condition: {weather['icon']} {weather['condition']}") + print(f"High/Low: {weather['high']}°C / {weather['low']}°C") + if weather['error']: + print(f"Error: {weather['error']}")