

Topic: Commission fines Temu €200 million for breaching the Digital Services Act
The following guide shows how to spin up a tiny, production‑ready service that polls a news API (e.g., NewsAPI.org) for the latest articles about the Temu DSA fine and prints (or can be extended to send) an alert.
Both Python and JavaScript/TypeScript implementations are provided, complete with setup, configuration, error handling, and a production‑readiness checklist.
What you need before you start coding:
| Item | Minimum Version | Why |
|---|---|---|
| Python | 3.9+ | Modern syntax, type hints, asyncio support |
| Node.js | 18.x (LTS) | Native fetch/async‑await, ES modules |
| Git | any | To clone the repo (optional) |
| NewsAPI.org account | Free tier (or paid) | Provides the news endpoint we query |
| IDE / Editor | VS Code, PyCharm, etc. | For development |
| Terminal | Bash, PowerShell, Zsh, etc. | To run commands |
Tip: If you don’t want to sign up for NewsAPI, you can replace the endpoint with any other news source (e.g., GNews, Mediastack) – just adjust the URL and query parameters accordingly.
Run the commands below in your project’s root folder.
mkdir regulatory-fine-alert
cd regulatory-fine-alert
# (optional) create a virtual environment
python -m venv .venv
source .venv/bin/activate # on Windows: .venv\Scripts\activate
# install dependencies
pip install --upgrade pip
pip install requests python-dotenv tqdm # tqdm for a nice progress bar (optional)
# Initialize a Node.js project (accept defaults)
npm init -y
# Install core libs
npm install axios dotenv
# (Optional) install TypeScript if you prefer TS
npm install --save-dev typescript @types/node ts-node
npx tsc --init # creates tsconfig.json
.env fileCreate a file named .env in the project root (never commit this to VCS!):
# .env – keep this secret!
NEWSAPI_KEY=your_newsapi_key_here
Security note: In production, load secrets from a secret manager (AWS Secrets Manager, GCP Secret Manager, HashiCorp Vault, etc.) instead of a plain
.envfile.
Both snippets do the same thing:
https://newsapi.org/v2/everythingFeel free to extend the logic to push alerts to Slack, email, or a monitoring system.
alert.py)#!/usr/bin/env python3
"""
regulatory-fine-alert: Python version
Fetches latest news about the Temu DSA fine from NewsAPI.org.
"""
import os
import sys
import time
import logging
from typing import List, Dict
import requests
from dotenv import load_dotenv
from tqdm import tqdm # optional progress bar
# ----------------------------------------------------------------------
# Configuration & Logging
# ----------------------------------------------------------------------
load_dotenv() # loads .env into os.environ
NEWSAPI_KEY = os.getenv("NEWSAPI_KEY")
if not NEWSAPI_KEY:
sys.exit("❌ ERROR: NEWSAPI_KEY not set in environment or .env file")
NEWS_ENDPOINT = "https://newsapi.org/v2/everything"
QUERY = '"Temu" AND ("Digital Services Act" OR "DSA" OR "€200 million")'
PARAMS = {
"q": QUERY,
"language": "en",
"sortBy": "publishedAt",
"pageSize": 10, # number of results per request
"apiKey": NEWSAPI_KEY,
}
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s %(levelname)s %(message)s",
datefmt="%H:%M:%S",
)
logger = logging.getLogger(__name__)
# ----------------------------------------------------------------------
# Core logic
# ----------------------------------------------------------------------
def fetch_news(retries: int = 3, backoff_factor: float = 1.0) -> List[Dict]:
"""
Call NewsAPI with exponential backoff retry.
Returns a list of article dicts (may be empty).
"""
for attempt in range(1, retries + 1):
try:
logger.info(f"Requesting news (attempt {attempt}/{retries})…")
resp = requests.get(NEWS_ENDPOINT, params=PARAMS, timeout=10)
resp.raise_for_status() # raises HTTPError for 4xx/5xx
data = resp.json()
if data.get("status") != "ok":
logger.error(f"API returned non-ok status: {data}")
return []
articles = data.get("articles", [])
logger.info(f"Fetched {len(articles)} article(s).")
return articles
except requests.RequestException as exc:
logger.warning(f"Request failed: {exc}")
if attempt < retries:
sleep_time = backoff_factor * (2 ** (attempt - 1))
logger.info(f"Retrying in {sleep_time:.1f}s…")
time.sleep(sleep_time)
else:
logger.error("Max retries reached – giving up.")
return []
except ValueError as json_err: # includes JSONDecodeError
logger.error(f"Failed to parse JSON response: {json_err}")
return []
def display_articles(articles: List[Dict]) -> None:
"""Pretty‑print a list of articles."""
if not articles:
logger.info("No articles to display.")
return
for i, art in enumerate(articles, start=1):
title = art.get("title", "(no title)")
source = art.get("source", {}).get("name", "unknown")
published = art.get("publishedAt", "unknown")
url = art.get("url", "#")
print(f"\n{i}. {title}")
print(f" Source: {source} | Published: {published}")
print(f" URL: {url}")
def main() -> None:
articles = fetch_news()
display_articles(articles)
if __name__ == "__main__":
main()
How to run
python alert.py
alert.ts)If you prefer plain JavaScript, rename the file to
alert.jsand drop the type annotations.
/**
* regulatory-fine-alert: TypeScript version
* Fetches latest news about the Temu DSA fine from NewsAPI.org.
*/
import axios from "axios";
import dotenv from "dotenv";
import { log, error } from "console";
// Load .env into process.env
dotenv.config();
const NEWSAPI_KEY: string | undefined = process.env.NEWSAPI_KEY;
if (!NEWSAPI_KEY) {
error("❌ ERROR: NEWSAPI_KEY not set in environment or .env file");
process.exit(1);
}
const NEWS_ENDPOINT = "https://newsapi.org/v2/everything";
const QUERY = '"Temu" AND ("Digital Services Act" OR "DSA" OR "€200 million")';
const PARAMS = {
q: QUERY,
language: "en",
sortBy: "publishedAt",
pageSize: 10,
apiKey: NEWSAPI_KEY,
};
/**
* Simple exponential backoff helper.
*/
async function sleep(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}
/**
* Fetch news with retry logic.
*/
async function fetchNews({
retries = 3,
backoffFactor = 1000,
}: {
retries?: number;
backoffFactor?: number;
} = {}): Promise<any[]> {
for (let attempt = 1; attempt <= retries; attempt++) {
try {
log(`🔎 Requesting news (attempt ${attempt}/${retries})…`);
const response = await axios.get(NEWS_ENDPOINT, {
params: PARAMS,
timeout: 10_000, // ms
});
if (response.data.status !== "ok") {
error(`❌ API returned non-ok status: ${JSON.stringify(response.data)}`);
return [];
}
const articles: any[] = response.data.articles ?? [];
log(`✅ Fetched ${articles.length} article(s).`);
return articles;
} catch (err: any) {
if (axios.isAxiosError(err)) {
const msg = err.response?.data?.message ?? err.message;
log(`⚠️ Request failed: ${msg}`);
} else {
log(`⚠️ Unexpected error: ${err}`);
}
if (attempt < retries) {
const delay = backoffFactor * 2 ** (attempt - 1);
log(`🕒 Retrying in ${delay / 1000}s…`);
await sleep(delay);
} else {
error("❌ Max retries reached – giving up.");
return [];
}
}
}
return []; // unreachable but keeps TS happy
}
/**
* Pretty‑print articles.
*/
function displayArticles(articles: any[]): void {
if (articles.length === 0) {
log("📭 No articles to display.");
return;
}
articles.forEach((art, idx) => {
const title = art.title ?? "(no title)";
const source = art.source?.name ?? "unknown";
const published = art.publishedAt ?? "unknown";
const url = art.url ?? "#";
log(`\n${idx + 1}. ${title}`);
log(` Source: ${source} | Published: ${published}`);
log(` URL: ${url}`);
});
}
/**
* Main entry point.
*/
async function main(): Promise<void> {
const articles = await fetchNews();
displayArticles(articles);
}
// Run if invoked directly
if (require.main === module) {
main().catch((err) => {
error("💥 Unhandled error:", err);
process.exit(1);
});
}
How to run
# If you kept it as .ts:
npx ts-node alert.ts
# Or compile to JS first:
npx tsc alert.ts
node alert.js
| Variable | Where to set | Example | Description |
|---|---|---|---|
NEWSAPI_KEY | .env (or secret manager) | abcd1234efgh5678ijkl9012mnop3456 | Your NewsAPI.org API key |
LOG_LEVEL (optional) | .env or export | INFO | Controls verbosity (DEBUG, INFO, WARN, ERROR) |
REQUEST_TIMEOUT (optional) | .env | 15 | Seconds before a request times out |
MAX_RETRIES (optional) | .env | 5 | How many times to retry a failed request |
Loading in code (shown in both snippets) – just read process.env / os.getenv.
In production replace the dotenv load with your platform’s secret retrieval (e.g., process.env.NEWSAPI_KEY injected by Kubernetes, AWS Lambda, etc.).
Below are reusable snippets you’ll likely copy‑paste into other projects.
def http_get_with_retry(url, params=None, *, retries=3, backoff=1, timeout=10):
for attempt in range(1, retries + 1):
try:
r = requests.get(url, params=params, timeout=timeout)
r.raise_for_status()
return r.json()
except requests.RequestException as e:
if attempt == retries:
raise
wait = backoff * (2 ** (attempt - 1))
time.sleep(wait)
import pLimit from "p-limit";
const limit = pLimit(2); // max 2 concurrent calls
async function guardedFetch(url: string, params: Record<string, any>) {
return limit(() => axios.get(url, { params, timeout: 8000 }));
}
// Usage
const data = await guardedFetch(NEWS_ENDPOINT, PARAMS);
import logging, json, sys
class JsonFormatter(logging.Formatter):
def format(self, record):
log_record = {
"timestamp": self.formatTime(record, self.datefmt),
"level": record.levelname,
"message": record.getMessage(),
"module": record.module,
}
return json.dumps(log_record)
handler = logging.StreamHandler(sys.stdout)
handler.setFormatter(JsonFormatter())
logger = logging.getLogger("myapp")
logger.setLevel(logging.INFO)
logger.addHandler(handler)
// Simple token bucket: allow N requests per minute
class RateLimiter {
private tokens: number;
private readonly max: number;
private readonly intervalMs: number;
private lastRefill: number;
constructor(maxPerMinute: number) {
this.max = maxPerMinute;
this.tokens = maxPerMinute;
this.intervalMs = 60_000 / maxPerMinute;
this.lastRefill = Date.now();
}
async acquire() {
now = Date.now();
this.tokens += Math.floor((now - this.lastRefill) / this.intervalMs);
this.lastRefill = now;
if (this.tokens > this.max) this.tokens = this.max;
if (this.tokens < 1) {
const wait = this.intervalMs - (now - this.lastRefill);
await new Promise((r) => setTimeout(r, wait));
return this.acquire();
}
this.tokens--;
}
}
| Symptom | Likely Cause | Fix |
|---|---|---|
401 Unauthorized | Missing or invalid NEWSAPI_KEY | Verify the key in .env; ensure no extra spaces; regenerate if needed |
429 Too Many Requests | Hitting NewsAPI rate limit (free tier: 100 req/day) | Reduce polling frequency, upgrade plan, or cache results for a few minutes |
ECONNREFUSED / network timeout | No internet, DNS issue, or proxy blocking | Check connectivity (curl https://newsapi.org), configure proxy via REQUESTS_CA_BUNDLE (Python) or https_proxy env (Node) |
| Empty article list | Query too restrictive or no recent news | Broaden the q parameter (e.g., just "Temu"), verify date range (from/to params) |
JSONDecodeError | Received HTML error page (e.g., 503) | Inspect response.text; add a check for Content-Type: application/json |
TypeScript compile errors (Cannot find name 'process') | Running TS without node types | Install @types/node (npm i -D @types/node) and ensure tsconfig.json has "types": ["node"] |
Module not found: 'dotenv' | Forgot to install dependency | Run npm install dotenv (or pip install python-dotenv) |
Debug tip: Add logger.debug(response.text) (Python) or console.log(response.data) (JS) right after the request to see the raw payload before parsing.
Before you push this to staging/production, run through the list below.
| ✅ Item | Why it matters |
|---|---|
Secret management – never commit .env; use AWS Secrets Manager, GCP Secret Manager, HashiCorp Vault, or Kubernetes Secrets. | |
| Structured logging – emit JSON logs to a central system (ELK, Splunk, CloudWatch). | |
Metrics & tracing – expose Prometheus counters (requests_total, errors_total) and latency histograms; optionally add OpenTelemetry tracing. | |
| Rate‑limit compliance – respect the provider’s limits; implement a token bucket or leaky bucket. | |
Circuit breaker – stop hammering a failing API (e.g., using opossum in Node or pybreaker in Python). | |
| Retry with jitter – add random jitter to backoff to avoid thundering herd. | |
Health endpoint – expose /healthz returning 200 when the service can reach the news API. | |
| Containerization – Dockerfile (see snippet below) for reproducible builds. | |
CI/CD pipeline – lint (flake8/eslint), type‑check (mypy/tsc), unit tests (pytest/jest), and automated deploy. | |
Automated tests – mock the news API (using responses in Python, nock or msw in Node) and assert error handling paths. | |
Dependency scanning – run safety (Python) or npm audit/trivy (Node) in CI. | |
Documentation – keep a README.md with run instructions, env var list, and architecture diagram. | |
| Observability alerts – set up alerts on error rate > 1% or latency > 2s for 5 min. | |
Graceful shutdown – handle SIGTERM/SIGINT to finish in‑flight requests before exiting. | |
Version pinning – lock dependencies (pip freeze > requirements.txt, package-lock.json). | |
| Legal / compliance – verify you have the right to store/re‑distribute news article text per the provider’s TOS. |
# ---- Base image ----
FROM python:3.12-slim
# ---- Install system deps (if any) ----
RUN apt-get update && apt-get install -y --no-install-recommends \
gcc libffi-dev && \
rm -rf /var/lib/apt/lists/*
# ---- Create app dir ----
WORKDIR /app
# ---- Install Python dependencies ----
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# ---- Copy source ----
COPY alert.py .
# ---- Runtime ----
ENV PYTHONUNBUFFERED=1
EXPOSE 8080 # if you add a health endpoint
CMD ["python", "alert.py"]
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build # if you have a build step
FROM node:20-alpine
WORKDIR /app
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/dist ./dist # or .ts files if you run ts-node in prod
ENV NODE_ENV=production
EXPOSE 3000
CMD ["node", "dist/alert.js"]
You now have a complete, copy‑and‑paste ready implementation in both Python and JavaScript/TypeScript that:
Feel free to extend the core (fetch_news) to push alerts to Slack, email, or a SIEM, or to store results in a time‑series DB for trend analysis. Happy coding, and may your alerts always be timely and accurate! 🚀
Source: EU AI Policy
Follow ICARAX for more AI insights and tutorials.
