<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: Jose Peinado</title>
    <description>The latest articles on Forem by Jose Peinado (@bambuco).</description>
    <link>https://forem.com/bambuco</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F914829%2F3d7d4736-ea3c-46fd-9efd-458982d27f9a.png</url>
      <title>Forem: Jose Peinado</title>
      <link>https://forem.com/bambuco</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/bambuco"/>
    <language>en</language>
    <item>
      <title>Five Years as an AWS Community Builder — What I’ve Learned and Why It Matters</title>
      <dc:creator>Jose Peinado</dc:creator>
      <pubDate>Mon, 02 Feb 2026 18:02:13 +0000</pubDate>
      <link>https://forem.com/bambuco/five-years-as-an-aws-community-builder-what-ive-learned-and-why-it-matters-1n9f</link>
      <guid>https://forem.com/bambuco/five-years-as-an-aws-community-builder-what-ive-learned-and-why-it-matters-1n9f</guid>
      <description>&lt;p&gt;This year marks my sixth renewal in the AWS Community Builders program. Looking back, it’s clear that this journey has influenced much more than just my technical skills. It changed how I learn, how I explain things, and how I interact with the broader tech community.&lt;/p&gt;

&lt;p&gt;I didn’t start with a plan to “build a personal brand” or to optimize for visibility. I started by writing things down — mostly to make sense of what I was working on. Over time, that habit evolved into something more structured, more intentional, and more community-oriented. This post is a reflection on that process, what I learned along the way, and how the way we consume and share technical knowledge is changing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Year One — Writing to Understand
&lt;/h2&gt;

&lt;p&gt;In the first year, writing was mostly a personal tool. I documented AWS setups, small experiments, and problems I had just solved. The audience was secondary. The main goal was clarity — for myself.&lt;/p&gt;

&lt;p&gt;What surprised me early on was how often someone else had the exact same problem. A simple post explaining why something broke or how I fixed it was sometimes more useful than official documentation. That was my first real feedback loop with the community.&lt;/p&gt;

&lt;h2&gt;
  
  
  Year Two — Consistency Beats Polish
&lt;/h2&gt;

&lt;p&gt;By the second year, I realized that consistency mattered more than perfectly crafted content. Posts tied directly to real work — infrastructure decisions, automation, debugging — tended to resonate more than theoretical explanations.&lt;/p&gt;

&lt;p&gt;I also started participating more actively in meetups and online discussions. Explaining something out loud has a way of exposing gaps in your understanding. That process, while uncomfortable at times, accelerated learning far more than passive reading.&lt;/p&gt;

&lt;h2&gt;
  
  
  Year Three — Community as a Multiplier
&lt;/h2&gt;

&lt;p&gt;The third year was when the community aspect became central. Events like AWS Community Days and virtual meetups created spaces where ideas circulated quickly. Content improved because it was discussed, challenged, and refined by others.&lt;/p&gt;

&lt;p&gt;At this stage, my posts shifted from “here’s how to do X” to “here’s how this behaves in real environments, and here are the trade-offs.” That shift came directly from conversations with other builders and practitioners.&lt;/p&gt;

&lt;h2&gt;
  
  
  Year Four — Sharing the Process, Not Just the Result
&lt;/h2&gt;

&lt;p&gt;In the fourth year, collaboration became more important than individual output. I spent more time reviewing drafts, giving feedback, and helping others structure their ideas.&lt;/p&gt;

&lt;p&gt;One key realization during this period was that showing the process — failed attempts, constraints, and decisions — was often more valuable than presenting a clean final solution. People learn faster when they understand why a choice was made, not just what was done.&lt;/p&gt;

&lt;h2&gt;
  
  
  Year Five — Learning in the Age of Chat Agents
&lt;/h2&gt;

&lt;p&gt;The fifth year coincided with a noticeable shift in how technical people search for and consume information.&lt;/p&gt;

&lt;p&gt;Chat-based agents changed the default behavior. Instead of starting with blog searches or documentation indexes, many engineers now start with a conversation. That doesn’t eliminate the need for written content — it changes its role.&lt;/p&gt;

&lt;p&gt;What I’ve noticed is this:&lt;br&gt;
• People still rely on blogs, docs, and talks, but often through an agent&lt;br&gt;
• Well-structured posts become reference material for AI-assisted exploration&lt;br&gt;
• Clear explanations, constraints, and real-world context matter more than ever&lt;/p&gt;

&lt;p&gt;In this environment, vague or shallow content quickly loses value. What remains useful is content grounded in experience: concrete examples, architectural reasoning, and lessons learned from production systems.&lt;/p&gt;

&lt;p&gt;As a Community Builder, this reinforced the importance of writing things that are durable — explanations that remain relevant even as tools and interfaces change.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the Program Has Meant in Practice
&lt;/h2&gt;

&lt;p&gt;Over these five years, the AWS Community Builders program provided:&lt;br&gt;
• Access to technical briefings and early insights&lt;br&gt;
• AWS credits to test ideas without friction&lt;br&gt;
• Certification vouchers that supported structured learning&lt;br&gt;
• A global network of people willing to share honestly&lt;/p&gt;

&lt;p&gt;But the most valuable part wasn’t any single benefit. It was the ongoing encouragement to keep learning in public, to document understanding, and to contribute back consistently.&lt;/p&gt;

&lt;h2&gt;
  
  
  Contribution Over Credentials
&lt;/h2&gt;

&lt;p&gt;A common misunderstanding is that the program rewards titles, follower counts, or perfectly curated content. In practice, what matters is contribution over time.&lt;/p&gt;

&lt;p&gt;That contribution can take many forms: writing, speaking, mentoring, answering questions, or sharing code. What connects them is usefulness. Content that helps others think more clearly or avoid mistakes tends to compound in value.&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing Thoughts
&lt;/h2&gt;

&lt;p&gt;Being an AWS Community Builder for five years hasn’t been about reaching a milestone. It’s been about building a habit: learning something, understanding it deeply enough to explain it, and sharing it in a way that helps others (most likely in personal events).&lt;/p&gt;

&lt;p&gt;As tools change — from search engines to chat agents — that habit remains valuable. Clear thinking, honest documentation, and community feedback still matter. Probably more than ever.&lt;/p&gt;

&lt;p&gt;If you’re considering applying, focus less on how it looks and more on how it helps. Share what you’re learning, not just what you’ve mastered. The rest tends to follow naturally.&lt;/p&gt;

</description>
      <category>renewal</category>
      <category>community</category>
      <category>aws</category>
    </item>
    <item>
      <title>Creating a Stock Price Alert App using AWS SAM and a Telegram Bot</title>
      <dc:creator>Jose Peinado</dc:creator>
      <pubDate>Sun, 23 Feb 2025 14:59:45 +0000</pubDate>
      <link>https://forem.com/bambuco/creating-a-stock-price-alert-app-using-aws-sam-and-a-telegram-bot-4hdf</link>
      <guid>https://forem.com/bambuco/creating-a-stock-price-alert-app-using-aws-sam-and-a-telegram-bot-4hdf</guid>
      <description>&lt;h1&gt;
  
  
  Overview
&lt;/h1&gt;

&lt;p&gt;This application allows users to create alerts for selected stock tickers on the NYSE. When the market price reaches or exceeds the target price, the application sends a notification directly to a Telegram chat via a bot. This way, we cover several serverless services and implement a common app flow to be used as a boilerplate.&lt;/p&gt;

&lt;p&gt;The solution uses:&lt;br&gt;
• AWS Lambda (Python) for backend logic:&lt;br&gt;
• Alerts Function: CRUD operations for alerts stored in DynamoDB.&lt;br&gt;
• Price Scanner Function: Scheduled function to query current stock prices via &lt;a href="https://pypi.org/project/yfinance/" rel="noopener noreferrer"&gt;yfinance&lt;/a&gt; and send Telegram messages.&lt;br&gt;
• Basic Authorizer: A simple Lambda authorizer to secure API Gateway endpoints.&lt;br&gt;
• Amazon DynamoDB to store alerts.&lt;br&gt;
• Amazon API Gateway to expose REST endpoints.&lt;br&gt;
• AWS SAM for deployment.&lt;br&gt;
• Amazon S3 for hosting the frontend static website.&lt;br&gt;
• Telegram Bot API for push notifications.&lt;/p&gt;
&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;p&gt;• AWS Account with permissions to deploy Lambda, DynamoDB, API Gateway, SNS, and S3 resources.&lt;br&gt;
• AWS SAM CLI installed and configured.&lt;br&gt;
• Python 3.9 environment.&lt;br&gt;
• Docker (for local SAM testing with container emulation).&lt;br&gt;
• A Telegram Bot created via &lt;a class="mentioned-user" href="https://dev.to/botfather"&gt;@botfather&lt;/a&gt; with a valid bot token.&lt;br&gt;
• A method to retrieve your Telegram Chat ID (by sending a message to the bot and calling the getUpdates API).&lt;/p&gt;
&lt;h2&gt;
  
  
  Project Structure
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F060yvrt8ah5d12fm83pp.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F060yvrt8ah5d12fm83pp.png" alt="Image description" width="637" height="190"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Github repository
&lt;/h2&gt;

&lt;p&gt;You can find this project on this Github repository:&lt;br&gt;
&lt;a href="https://github.com/joseapeinado/stocks-alerts-app" rel="noopener noreferrer"&gt;https://github.com/joseapeinado/stocks-alerts-app&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Step 1. Develop the Backend Functions
&lt;/h2&gt;
&lt;h3&gt;
  
  
  1.1 Alerts Function (alerts.py)
&lt;/h3&gt;

&lt;p&gt;This function implements REST endpoints to list, create, update, and delete alerts in DynamoDB. It also integrates with &lt;a href="https://pypi.org/project/yfinance/" rel="noopener noreferrer"&gt;yfinance&lt;/a&gt; to retrieve the current stock price when listing alerts.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;backend/handlers/alerts.py&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import os
import json
import uuid
from decimal import Decimal
import boto3
from datetime import datetime
import yfinance as yf

dynamodb = boto3.resource('dynamodb')
table = dynamodb.Table(os.environ['ALERTS_TABLE'])
sns = boto3.client('sns')
sns_topic_arn = os.environ['SNS_TOPIC_ARN']

def lambda_handler(event, context):
    try:
        resource = event.get("resource", "")
        http_method = event.get("httpMethod", "")

        if resource == "/alerts":
            if http_method == "GET":
                result = table.scan()
                items = result.get("Items", [])
                for item in items:
                    item["currentPrice"] = str(get_current_price(item.get("ticker", "")))
                return _response(200, items)

            elif http_method == "POST":
                body = json.loads(event["body"])
                alert = {
                    "alertId": str(uuid.uuid4()),
                    "ticker": body["ticker"],
                    "price": str(Decimal(str(body["price"]))),
                    "createdAt": datetime.utcnow().isoformat(),
                    "paused": False  # Default state: active
                }
                table.put_item(Item=alert)
                alert["currentPrice"] = str(get_current_price(alert["ticker"]))
                return _response(201, alert)

            elif http_method == "PUT":
                # Update price and/or paused state.
                body = json.loads(event["body"])
                alert_id = body["alertId"]
                update_expression = []
                expression_attribute_values = {}

                if "price" in body:
                    update_expression.append("price = :p")
                    expression_attribute_values[":p"] = str(Decimal(str(body["price"])))
                if "paused" in body:
                    update_expression.append("paused = :s")
                    expression_attribute_values[":s"] = bool(body["paused"])
                if not update_expression:
                    return _response(400, {"error": "No valid fields to update."})
                update_expr = "set " + ", ".join(update_expression)
                result = table.update_item(
                    Key={"alertId": alert_id},
                    UpdateExpression=update_expr,
                    ExpressionAttributeValues=expression_attribute_values,
                    ReturnValues="ALL_NEW"
                )
                updated_item = result.get("Attributes", {})
                updated_item["currentPrice"] = str(get_current_price(updated_item.get("ticker", "")))
                return _response(200, updated_item)

            elif http_method == "DELETE":
                alert_id = event["queryStringParameters"].get("alertId")
                table.delete_item(Key={"alertId": alert_id})
                return _response(200, {"message": "Alert deleted"})

            else:
                return _response(405, "Method Not Allowed")
        else:
            return _response(404, "Not Found")

    except Exception as e:
        return _response(500, {"error": str(e)})

def get_current_price(ticker):
    try:
        data = yf.Ticker(ticker)
        current_price = data.info.get("regularMarketPrice")
        return Decimal(str(current_price)) if current_price is not None else Decimal("0")
    except Exception:
        return Decimal("0")

def _response(status_code, body):
    return {
        "statusCode": status_code,
        "headers": {
            "Content-Type": "application/json",
            "Access-Control-Allow-Origin": "*"
        },
        "body": json.dumps(body, default=str)
    }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  1.2 Price Scanner Function (priceScanner.py)
&lt;/h3&gt;

&lt;p&gt;This function is scheduled to run periodically. It scans the AlertsTable, checks the current stock price using yfinance, and if the price meets or exceeds the target, it sends a message via Telegram. It also skips alerts that are paused.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;backend/handlers/priceScanner.py&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
import os
import json
from decimal import Decimal
import boto3
import yfinance as yf
from datetime import datetime
import logging
import requests

# Configure logging
logger = logging.getLogger()
logger.setLevel(logging.INFO)

dynamodb = boto3.resource('dynamodb')
table = dynamodb.Table(os.environ['ALERTS_TABLE'])

# Telegram Bot credentials from environment variables
TELEGRAM_BOT_TOKEN = os.environ.get("TELEGRAM_BOT_TOKEN")
TELEGRAM_CHAT_ID = os.environ.get("TELEGRAM_CHAT_ID")

def lambda_handler(event, context):
    result = table.scan()
    alerts = result.get("Items", [])
    triggered_alerts = []
    now = datetime.utcnow()

    for alert in alerts:
        # Skip paused alerts
        if alert.get("paused", False):
            continue

        ticker = alert.get("ticker")
        target_price = Decimal(alert.get("price"))
        current_price = get_current_price(ticker)

        # Trigger if the current price meets or exceeds the target
        if current_price &amp;gt;= target_price:
            message = (
                f"Alert: {ticker} current price ${current_price} meets/exceeds your target of ${target_price}."
            )
            send_telegram_message(message)
            triggered_alerts.append(alert)
            logger.info("Triggered Alert: %s", json.dumps({
                "alertId": alert.get("alertId"),
                "ticker": ticker,
                "targetPrice": str(target_price),
                "currentPrice": str(current_price),
                "triggeredAt": now.isoformat()
            }))

    return {
        "statusCode": 200,
        "body": json.dumps({
            "message": "Price scan completed",
            "triggeredAlerts": triggered_alerts
        }, default=str)
    }

def get_current_price(ticker):
    try:
        data = yf.Ticker(ticker)
        current_price = data.info.get("regularMarketPrice")
        return Decimal(str(current_price)) if current_price is not None else Decimal("0")
    except Exception as e:
        logger.error("Error retrieving price for %s: %s", ticker, e)
        return Decimal("0")

def send_telegram_message(message):
    if not TELEGRAM_BOT_TOKEN or not TELEGRAM_CHAT_ID:
        logger.error("Telegram bot token or chat id not configured.")
        return

    url = f"https://api.telegram.org/bot{TELEGRAM_BOT_TOKEN}/sendMessage"
    payload = {
        "chat_id": TELEGRAM_CHAT_ID,
        "text": message
    }
    try:
        response = requests.get(url, params=payload)
        logger.info("Telegram response: %s", response.text)
    except Exception as e:
        logger.error("Error sending Telegram message: %s", e)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note: To address a cache warning from yfinance, add the following after importing yfinance:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;yf.set_tz_cache_location('/tmp/py-yfinance')
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  1.3 Basic Authorizer (basicAuthorizer.py)
&lt;/h3&gt;

&lt;p&gt;This minimalistic authorizer validates the incoming Bearer token. In production, store sensitive values securely.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;backend/handlers/basicAuthorizer.py&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import json
import logging

logger = logging.getLogger()
logger.setLevel(logging.INFO)

def lambda_handler(event, context):
    logger.info("Received event: %s", json.dumps(event))
    try:
        token = event.get("authorizationToken", "")
        expected_token = "secret-token-123"  # Replace or secure this token as needed

        effect = "Allow" if token == f"Bearer {expected_token}" else "Deny"

        auth_response = {
            "principalId": "user",
            "policyDocument": {
                "Version": "2012-10-17",
                "Statement": [
                    {
                        "Action": "execute-api:Invoke",
                        "Effect": effect,
                        "Resource": event.get("methodArn")
                    }
                ]
            }
        }
        logger.info("Authorization response: %s", json.dumps(auth_response))
        return auth_response
    except Exception as e:
        logger.error("Error in authorizer", exc_info=True)
        raise e
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Requirements file
&lt;/h3&gt;

&lt;p&gt;Please create a requirements.txt file to manage Python dependencies, with the following modules:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;yfinance
requests
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 2. Develop the Frontend
&lt;/h2&gt;

&lt;h3&gt;
  
  
  2.1 HTML (index.html)
&lt;/h3&gt;

&lt;p&gt;This basic HTML page hosts a form for creating alerts and a table to display alerts with actions to update, delete, or toggle pause.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html lang="en"&amp;gt;
&amp;lt;head&amp;gt;
  &amp;lt;meta charset="UTF-8"&amp;gt;
  &amp;lt;title&amp;gt;Stock Price Alerts&amp;lt;/title&amp;gt;
  &amp;lt;link rel="stylesheet" href="styles.css"&amp;gt;
&amp;lt;/head&amp;gt;
&amp;lt;body&amp;gt;
  &amp;lt;div class="container"&amp;gt;
    &amp;lt;h1&amp;gt;Stock Price Alerts&amp;lt;/h1&amp;gt;
    &amp;lt;form id="alertForm"&amp;gt;
      &amp;lt;input type="text" id="ticker" placeholder="Ticker" required&amp;gt;
      &amp;lt;span id="currentPriceDisplay"&amp;gt;&amp;lt;/span&amp;gt;
      &amp;lt;input type="number" id="price" placeholder="Target Price" step="0.01" required&amp;gt;
      &amp;lt;button type="submit"&amp;gt;Add Alert&amp;lt;/button&amp;gt;
    &amp;lt;/form&amp;gt;
    &amp;lt;h2&amp;gt;Your Alerts&amp;lt;/h2&amp;gt;
    &amp;lt;table id="alertsTable"&amp;gt;
      &amp;lt;thead&amp;gt;
        &amp;lt;tr&amp;gt;
          &amp;lt;th&amp;gt;Ticker&amp;lt;/th&amp;gt;
          &amp;lt;th&amp;gt;Target Price&amp;lt;/th&amp;gt;
          &amp;lt;th&amp;gt;Current Price&amp;lt;/th&amp;gt;
          &amp;lt;th&amp;gt;Created At&amp;lt;/th&amp;gt;
          &amp;lt;th&amp;gt;State&amp;lt;/th&amp;gt;
          &amp;lt;th&amp;gt;Actions&amp;lt;/th&amp;gt;
        &amp;lt;/tr&amp;gt;
      &amp;lt;/thead&amp;gt;
      &amp;lt;tbody id="alertsTableBody"&amp;gt;&amp;lt;/tbody&amp;gt;
    &amp;lt;/table&amp;gt;
  &amp;lt;/div&amp;gt;
  &amp;lt;script src="main.js"&amp;gt;&amp;lt;/script&amp;gt;
&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2.2 JavaScript (main.js)
&lt;/h3&gt;

&lt;p&gt;The JavaScript handles user interactions: fetching the current price on ticker blur, creating alerts, and updating/deleting/toggling alert state via API calls.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
const apiUrl = 'https://&amp;lt;YOUR_API_GATEWAY_ID&amp;gt;.execute-api.&amp;lt;region&amp;gt;.amazonaws.com/Prod/alerts';
const priceUrl = 'https://&amp;lt;YOUR_API_GATEWAY_ID&amp;gt;.execute-api.&amp;lt;region&amp;gt;.amazonaws.com/Prod/price';
const authHeader = 'Bearer secret-token-123';

document.addEventListener('DOMContentLoaded', () =&amp;gt; {
  loadAlerts();

  // When the ticker field loses focus, show the current price.
  const tickerInput = document.getElementById('ticker');
  tickerInput.addEventListener('blur', async () =&amp;gt; {
    const ticker = tickerInput.value.trim();
    const display = document.getElementById('currentPriceDisplay');
    if (ticker) {
      try {
        const response = await fetch(`${priceUrl}?ticker=${encodeURIComponent(ticker)}`, {
          method: 'GET',
          headers: { 'Authorization': authHeader }
        });
        const data = await response.json();
        if (response.ok &amp;amp;&amp;amp; data.currentPrice) {
          display.textContent = `Current Price: $${data.currentPrice}`;
        } else {
          display.textContent = 'Current Price: N/A';
        }
      } catch (err) {
        display.textContent = 'Current Price: Error';
      }
    } else {
      display.textContent = '';
    }
  });
});

document.getElementById('alertForm').addEventListener('submit', async (e) =&amp;gt; {
  e.preventDefault();
  const ticker = document.getElementById('ticker').value;
  const price = parseFloat(document.getElementById('price').value);

  const response = await fetch(apiUrl, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'Authorization': authHeader
    },
    body: JSON.stringify({ ticker, price })
  });
  if (response.ok) {
    loadAlerts();
    e.target.reset();
    document.getElementById('currentPriceDisplay').textContent = '';
  }
});

async function loadAlerts() {
  const response = await fetch(apiUrl, {
    method: 'GET',
    headers: { 'Authorization': authHeader }
  });
  const alerts = await response.json();
  const tableBody = document.getElementById('alertsTableBody');
  tableBody.innerHTML = '';

  alerts.forEach(alert =&amp;gt; {
    const tr = document.createElement('tr');

    // Ticker cell
    const tdTicker = document.createElement('td');
    tdTicker.textContent = alert.ticker;
    tr.appendChild(tdTicker);

    // Target Price cell
    const tdPrice = document.createElement('td');
    tdPrice.textContent = alert.price;
    tr.appendChild(tdPrice);

    // Current Price cell
    const tdCurrentPrice = document.createElement('td');
    tdCurrentPrice.textContent = alert.currentPrice || "N/A";
    tr.appendChild(tdCurrentPrice);

    // Created At cell
    const tdCreatedAt = document.createElement('td');
    tdCreatedAt.textContent = alert.createdAt;
    tr.appendChild(tdCreatedAt);

    // State cell (Active or Paused)
    const tdState = document.createElement('td');
    tdState.textContent = alert.paused ? "Paused" : "Active";
    tr.appendChild(tdState);

    // Actions cell with three buttons: Update Price, Delete, Toggle Pause
    const tdActions = document.createElement('td');
    tdActions.innerHTML = `
      &amp;lt;button class="icon-button" title="Update Price" onclick='updateAlert(${JSON.stringify(alert)})'&amp;gt;
        &amp;lt;svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24"&amp;gt;
          &amp;lt;path d="M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04a1 1 0 000-1.41l-2.34-2.34a1 1 0 00-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83z" fill="currentColor"/&amp;gt;
        &amp;lt;/svg&amp;gt;
      &amp;lt;/button&amp;gt;
      &amp;lt;button class="icon-button" title="Delete" onclick='deleteAlert("${alert.alertId}")'&amp;gt;
        &amp;lt;svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24"&amp;gt;
          &amp;lt;path d="M16 9v10H8V9h8m-1.5-6h-5l-1 1H5v2h14V4h-4.5l-1-1z" fill="currentColor"/&amp;gt;
        &amp;lt;/svg&amp;gt;
      &amp;lt;/button&amp;gt;
      &amp;lt;button class="icon-button" title="Toggle Pause" onclick='toggleAlertState(${JSON.stringify(alert)})'&amp;gt;
        &amp;lt;svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24"&amp;gt;
          &amp;lt;path d="M12 2a10 10 0 100 20 10 10 0 000-20zm1 14h-2v-4h2v4zm0-6h-2V7h2v3z" fill="currentColor"/&amp;gt;
        &amp;lt;/svg&amp;gt;
      &amp;lt;/button&amp;gt;
    `;
    tr.appendChild(tdActions);

    tableBody.appendChild(tr);
  });
}

async function updateAlert(alert) {
  const newPrice = prompt('Enter new price', alert.price);
  if (newPrice) {
    const response = await fetch(apiUrl, {
      method: 'PUT',
      headers: {
        'Content-Type': 'application/json',
        'Authorization': authHeader
      },
      body: JSON.stringify({
        alertId: alert.alertId,
        ticker: alert.ticker,
        price: parseFloat(newPrice)
      })
    });
    if (response.ok) {
      loadAlerts();
    } else {
      alert('Update failed.');
    }
  }
}

async function deleteAlert(alertId) {
  if (confirm('Are you sure you want to delete this alert?')) {
    const response = await fetch(`${apiUrl}?alertId=${alertId}`, {
      method: 'DELETE',
      headers: { 'Authorization': authHeader }
    });
    if (response.ok) {
      loadAlerts();
    } else {
      alert('Deletion failed.');
    }
  }
}

async function toggleAlertState(alert) {
  const newPaused = !alert.paused;
  const response = await fetch(apiUrl, {
    method: 'PUT',
    headers: {
      'Content-Type': 'application/json',
      'Authorization': authHeader
    },
    body: JSON.stringify({
      alertId: alert.alertId,
      ticker: alert.ticker,
      price: alert.price,
      paused: newPaused
    })
  });
  if (response.ok) {
    loadAlerts();
  } else {
    alert('Toggle failed.');
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2.3 CSS (styles.css)
&lt;/h3&gt;

&lt;p&gt;Ensure your icons and table display correctly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
/* Table styling */
table {
  width: 100%;
  border-collapse: collapse;
  margin-top: 1rem;
}

th, td {
  border: 1px solid #ddd;
  padding: 8px;
  text-align: left;
}

th {
  background-color: #f4f4f4;
}

/* Icon button styling */
.icon-button {
  background: none;
  border: none;
  cursor: pointer;
  padding: 4px;
  margin: 0 2px;
  color: #000; /* Ensure currentColor is visible */
}

.icon-button svg {
  fill: currentColor;
}

.icon-button:hover svg {
  fill: #0073e6;
}

/* Current price display next to ticker input */
#currentPriceDisplay {
  margin-left: 10px;
  font-weight: bold;
  color: #333;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 3. SAM Template
&lt;/h2&gt;

&lt;p&gt;Below is the final template.yaml for the application. It provisions all resources (DynamoDB, SNS, Lambda functions, API Gateway, S3 bucket for frontend) and configures environment variables and policies.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: Minimal Stock Price Alerts WebApp with Python Lambdas, CORS, S3 static website, and Outputs

Parameters:
  Environment:
    Type: String
    Default: Prod
    AllowedValues:
      - Prod
      - Dev
    Description: Environment type

Globals:
  Function:
    Timeout: 10

Resources:
  AlertsTable:
    Type: AWS::DynamoDB::Table
    Properties:
      TableName: AlertsTable
      AttributeDefinitions:
        - AttributeName: alertId
          AttributeType: S
      KeySchema:
        - AttributeName: alertId
          KeyType: HASH
      BillingMode: PAY_PER_REQUEST

  SNSTopic:
    Type: AWS::SNS::Topic
    Properties:
      TopicName: StockAlertsTopic

  AlertsFunction:
    Type: AWS::Serverless::Function
    Properties:
      CodeUri: backend/handlers/
      Handler: alerts.lambda_handler
      Runtime: python3.9
      Policies:
        - DynamoDBCrudPolicy:
            TableName: !Ref AlertsTable
      Environment:
        Variables:
          ALERTS_TABLE: !Ref AlertsTable
          SNS_TOPIC_ARN: !Ref SNSTopic
      Events:
        AlertsAPI:
          Type: Api
          Properties:
            Path: /alerts
            Method: any
            RestApiId: !Ref AlertsApi
        PriceLookup:
          Type: Api
          Properties:
            Path: /price
            Method: GET
            RestApiId: !Ref AlertsApi

  PriceScannerFunction:
    Type: AWS::Serverless::Function
    Properties:
      CodeUri: backend/handlers/
      Handler: priceScanner.lambda_handler
      Runtime: python3.9
      Timeout: 30    # Increased timeout for external API calls
      Policies:
        - DynamoDBCrudPolicy:
            TableName: !Ref AlertsTable
        - SNSPublishMessagePolicy:
            TopicName: StockAlertsTopic
      Environment:
        Variables:
          ALERTS_TABLE: !Ref AlertsTable
          SNS_TOPIC_ARN: !GetAtt SNSTopic.TopicArn
          TELEGRAM_CHAT_ID: "REDACTED"
          TELEGRAM_BOT_TOKEN: "REDACTED"
      Events:
        PriceScannerSchedule:
          Type: Schedule
          Properties:
            Schedule: rate(10 minutes)

  EmailSubscription1:
    Type: AWS::SNS::Subscription
    Properties:
      TopicArn: !Ref SNSTopic
      Protocol: email
      Endpoint: "joseapeinado@gmail.com"

  BasicAuthAuthorizerFunction:
    Type: AWS::Serverless::Function
    Properties:
      CodeUri: backend/handlers/
      Handler: basicAuthorizer.lambda_handler
      Runtime: python3.9
      MemorySize: 128
      Timeout: 5

  AlertsApi:
    Type: AWS::Serverless::Api
    Properties:
      StageName: Prod
      Cors:
        AllowOrigin: "'*'"
        AllowHeaders: "'Content-Type,Authorization'"
        AllowMethods: "'OPTIONS,GET,POST,PUT,DELETE'"
      Auth:
        DefaultAuthorizer: BasicAuthorizer
        AddDefaultAuthorizerToCorsPreflight: false
        Authorizers:
          BasicAuthorizer:
            FunctionArn: !GetAtt BasicAuthAuthorizerFunction.Arn
            Identity:
              ReauthorizeEvery: 0
              Headers:
                - Authorization

  StaticSiteBucket:
    Type: AWS::S3::Bucket
    Properties:
      BucketName: stocks-alerts-app-staticsitebucket-12345678
      WebsiteConfiguration:
        IndexDocument: index.html
      PublicAccessBlockConfiguration:
        BlockPublicAcls: false
        BlockPublicPolicy: false
        IgnorePublicAcls: false
        RestrictPublicBuckets: false

  StaticSiteBucketPolicy:
    Type: AWS::S3::BucketPolicy
    Properties:
      Bucket: !Ref StaticSiteBucket
      PolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Sid: PublicReadGetObject
            Effect: Allow
            Principal: "*"
            Action: s3:GetObject
            Resource: !Sub "${StaticSiteBucket.Arn}/*"

Outputs:
  ApiUrl:
    Description: "API Gateway endpoint URL for the Prod stage"
    Value: !Sub "https://${AlertsApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/alerts"
  AlertsTableName:
    Description: "DynamoDB Alerts Table Name"
    Value: !Ref AlertsTable
  SNSTopicARN:
    Description: "SNS Topic ARN"
    Value: !Ref SNSTopic
  StaticSiteURL:
    Description: "URL for static website hosted on S3"
    Value: !GetAtt StaticSiteBucket.WebsiteURL

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 4. Build, Deploy, and Test
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Build the Application:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sam build
Building codeuri: /Users/josepeinado/jose/projects/serverless/stocks-alerts-app/backend/handlers runtime: python3.9 architecture: x86_64 functions: AlertsFunction, PriceScannerFunction, BasicAuthAuthorizerFunction                                                                                                                                                                                                                                                                              
 Running PythonPipBuilder:ResolveDependencies                                                                                                                                                                                                                                                                                                                                                                                                                                                      
 Running PythonPipBuilder:CopySource                                                                                                                                                                                                                                                                                                                                                                                                                                                               

Build Succeeded

Built Artifacts  : .aws-sam/build
Built Template   : .aws-sam/build/template.yaml

Commands you can use next
=========================
[*] Validate SAM template: sam validate
[*] Invoke Function: sam local invoke
[*] Test Function in the Cloud: sam sync --stack-name {{stack-name}} --watch
[*] Deploy: sam deploy --guided
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;Deploy the Application:
You can use a configuration file (samconfig.toml):
samconfig.toml
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;version = 0.1
[default.deploy.parameters]
stack_name = "stocks-alerts-app"
resolve_s3 = true
s3_prefix = "stocks-alerts-app"
region = "us-east-1"
confirm_changeset = false
capabilities = "CAPABILITY_IAM"
image_repositories = []
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sam deploy 

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;Deploy the Frontend:
Sync the frontend/ folder to your S3 bucket:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;aws s3 sync frontend/ s3://stocks-alerts-app-staticsitebucket-12345678 --delete

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;Testing:
• Use your browser to load the static site URL.
• Create alerts for specific tickers.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fc05kr0o0rsaa9mw44alb.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fc05kr0o0rsaa9mw44alb.png" alt="Image description" width="652" height="396"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4vvpg4rvlgomsa30vg6s.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4vvpg4rvlgomsa30vg6s.png" alt="Image description" width="662" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;• Verify via the Telegram chat that alerts are sent when the price condition is met. At this moment, I chose to trigger the alert when the target price is equal to or above the current price, so please adjust this to avoid getting alerts every time :).&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7ggz46acmujheivnwrcy.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7ggz46acmujheivnwrcy.png" alt="Image description" width="800" height="128"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;• Use the table to update, delete, or toggle pause state on alerts.&lt;/p&gt;

&lt;h3&gt;
  
  
  Rationale &amp;amp; Considerations
&lt;/h3&gt;

&lt;p&gt;• Serverless Architecture:&lt;br&gt;
Leveraging AWS Lambda, API Gateway, and DynamoDB allows for a highly scalable and cost-effective solution. AWS SAM streamlines deployment and resource management.&lt;br&gt;
• Python &amp;amp; yfinance:&lt;br&gt;
Python provides a concise syntax and powerful libraries. The yfinance library lets us retrieve real-time market data without a paid API, though note it’s unofficial.&lt;br&gt;
• Telegram Integration:&lt;br&gt;
Using Telegram Bot API enables real-time push notifications. This is particularly useful for immediate alerting without relying on email (which might be slower or filtered).&lt;br&gt;
• Security:&lt;br&gt;
The Basic Authorizer secures API endpoints. In production, consider using a more robust authentication mechanism and store sensitive tokens securely (e.g., AWS Secrets Manager).&lt;br&gt;
• Extensibility:&lt;br&gt;
The design supports easy extension. For example, additional endpoints, richer alert criteria, or integration with other notification systems can be added with minimal changes.&lt;/p&gt;

</description>
      <category>sam</category>
      <category>telegram</category>
      <category>serverless</category>
      <category>stocks</category>
    </item>
    <item>
      <title>Moving data between Elasticache Databases</title>
      <dc:creator>Jose Peinado</dc:creator>
      <pubDate>Mon, 26 Sep 2022 13:14:01 +0000</pubDate>
      <link>https://forem.com/bambuco/moving-data-between-elasticache-databases-1e95</link>
      <guid>https://forem.com/bambuco/moving-data-between-elasticache-databases-1e95</guid>
      <description>&lt;h2&gt;
  
  
  The problem
&lt;/h2&gt;

&lt;p&gt;Some time ago, me and my team were asked to migrate several Elasticache databases (Redis) from EC2-Classic (yep, I know 🤷‍♂️) to VPC. All these Databases were being used in production services so, taking an snapshot and restoring it on a VPC was not a good idea due to the time that takes doing the whole process.&lt;br&gt;
Using &lt;a href="https://docs.aws.amazon.com/AmazonElastiCache/latest/red-ug/OnlineMigration.html"&gt;AWS Online Migration&lt;/a&gt; was not an option too, since Online migration is designed for data migration from hosted Redis on Amazon EC2 or on-premise self-hosted Redis to ElastiCache for Redis and not between ElastiCache for Redis clusters. &lt;br&gt;
So we kept looking for alternatives until we found a nice &lt;a href="https://gist.github.com/kitwalker12/517d99c3835975ad4d1718d28a63553e"&gt;gist&lt;/a&gt; with a script that almost fit our needs. The only fail there was that it wasn't work ok on encryption in transit clusters.&lt;/p&gt;

&lt;h2&gt;
  
  
  The solution
&lt;/h2&gt;

&lt;p&gt;So we did some changes on the script to fit our use-case and et voila! We finaly got a &lt;a href="https://gist.github.com/joseapeinado/fb0deb31793fe32b37ee6b5d38cc3761"&gt;working version&lt;/a&gt; to move the data.&lt;/p&gt;

&lt;h1&gt;
  
  
  Demo
&lt;/h1&gt;

&lt;p&gt;From a terminal with access to both Elasticache clusters (very important!): &lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Clone the repo:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;git clone git@github.com:joseapeinado/ElasticacheMigration.git
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;Prepare the virtual env and install dependencies
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd &lt;/span&gt;ElasticacheMigration
python3 &lt;span class="nt"&gt;-m&lt;/span&gt; venv venv
&lt;span class="nb"&gt;source &lt;/span&gt;venv/bin/activate
pip3 &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; requirements.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;Profit!!!
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;SRC_CLUSTER&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;src_cluster.spznk9.ng.0001.use1.cache.amazonaws.com
&lt;span class="nv"&gt;DST_CLUSTER&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;dst_cluster.spznk9.ng.0001.use1.cache.amazonaws.com

python3 migrate-redis.py &lt;span class="nv"&gt;$SRC_CLUSTER&lt;/span&gt; &lt;span class="nt"&gt;--src_port&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;6379 &lt;span class="nv"&gt;$DST_CLUSTER&lt;/span&gt; &lt;span class="nt"&gt;--dst_port&lt;/span&gt; 6379 &lt;span class="nt"&gt;--src_db&lt;/span&gt; 2 &lt;span class="nt"&gt;--dst_db&lt;/span&gt; 2
Connecting to Redis instances...
Counting keys to migrate...
19222 keys: 100% |###############################################################################################################################################################################################################################################| Time: 0:00:05
&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Keys disappeared on source during scan:'&lt;/span&gt;, 0&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Keys already existing on destination:'&lt;/span&gt;, 19195&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Use cases
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Copying just one Database to other existing cluster
&lt;/h3&gt;

&lt;p&gt;The script copies data between clusters in a per-database basis, so it is ready to perform this migration out-of-the-box.&lt;/p&gt;

&lt;h3&gt;
  
  
  Copy all databases from one cluster to other
&lt;/h3&gt;

&lt;p&gt;The only caveat here is the need of run the migration script once per database.&lt;/p&gt;

&lt;h3&gt;
  
  
  Copy database data from one DB to other DB within the same cluster.
&lt;/h3&gt;

&lt;h2&gt;
  
  
  Advantages doing this migration
&lt;/h2&gt;

&lt;p&gt;The more obvious advantage I think is the time that could take using the script versus:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Stop writes on Redis cluster&lt;/li&gt;
&lt;li&gt;Take a snapshot&lt;/li&gt;
&lt;li&gt;Launch a new cluster from the snapshot&lt;/li&gt;
&lt;li&gt;Resume writes on the new cluster&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This process can take at least 15 minutes or so. &lt;/p&gt;

&lt;h2&gt;
  
  
  Contribute
&lt;/h2&gt;

&lt;p&gt;Any improvements will be more than welcomed 😃&lt;br&gt;
&lt;a href="https://github.com/joseapeinado/ElasticacheMigration/"&gt;https://github.com/joseapeinado/ElasticacheMigration/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is my very first tech post, so any advice would be appreciated. &lt;br&gt;
And I hope the migration script help you like it helped me 🙂&lt;/p&gt;

</description>
      <category>elasticache</category>
      <category>redis</category>
      <category>python</category>
    </item>
  </channel>
</rss>
