<?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>DEV Community: beretests</title>
    <description>The latest articles on DEV Community by beretests (@beretests).</description>
    <link>https://dev.to/beretests</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%2F889851%2Fd1f9dd11-4e9a-4925-afe0-4208681bc98f.png</url>
      <title>DEV Community: beretests</title>
      <link>https://dev.to/beretests</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/beretests"/>
    <language>en</language>
    <item>
      <title>Perfect Landing: Goat Cheese Day</title>
      <dc:creator>beretests</dc:creator>
      <pubDate>Sat, 28 Jun 2025 15:59:02 +0000</pubDate>
      <link>https://dev.to/beretests/perfect-landing-goat-cheese-day-hp6</link>
      <guid>https://dev.to/beretests/perfect-landing-goat-cheese-day-hp6</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for &lt;a href="https://dev.to/challenges/frontend-2025-06-04"&gt;Frontend Challenge - June Celebrations, Perfect Landing: June Celebrations&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;I built a landing page to celebrate &lt;em&gt;&lt;strong&gt;Goat Cheese Day (June 25)&lt;/strong&gt;&lt;/em&gt;. The goal was to highlight the joy, diversity, and community around goat cheese. The page features a modern, responsive design built with React and Material UI. It includes sections about the origins and benefits of goat cheese, the different types, recipes and pairings, FAQs, and upcoming events. Users can sign up for a newsletter, download a recipe booklet, and explore interactive content — all designed to promote and celebrate Goat Cheese Day in a fun and informative way.&lt;/p&gt;

&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

&lt;p&gt;For a limited time, this landing page can be viewed live at &lt;a href="https://goat-cheese.beretesting.com" rel="noopener noreferrer"&gt;Goat Cheese Day Page&lt;/a&gt;. Please feel free to interact with the page and let me know what you think!&lt;/p&gt;

&lt;p&gt;This project code can also be viewed on Github below:&lt;br&gt;
&lt;/p&gt;
&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fassets.dev.to%2Fassets%2Fgithub-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/beretests" rel="noopener noreferrer"&gt;
        beretests
      &lt;/a&gt; / &lt;a href="https://github.com/beretests/devto-challenge-june-celebrations-2025" rel="noopener noreferrer"&gt;
        devto-challenge-june-celebrations-2025
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;
&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;DevTo Challenge: June Celebrations 2025 (Goat Cheese Day) – Vite + React + Material UI + Tailwind CSS&lt;/h1&gt;
&lt;/div&gt;
&lt;p&gt;Celebrate Goat Cheese Day with this interactive, modern web app built using &lt;a href="https://vitejs.dev/" rel="nofollow noopener noreferrer"&gt;Vite&lt;/a&gt;, &lt;a href="https://react.dev/" rel="nofollow noopener noreferrer"&gt;React&lt;/a&gt;, &lt;a href="https://mui.com/" rel="nofollow noopener noreferrer"&gt;Material UI&lt;/a&gt;, &lt;a href="https://tailwindcss.com/" rel="nofollow noopener noreferrer"&gt;Tailwind CSS&lt;/a&gt;, and &lt;a href="https://zustand-demo.pmnd.rs/" rel="nofollow noopener noreferrer"&gt;Zustand&lt;/a&gt;.&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Features&lt;/h2&gt;
&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;Responsive design with Material UI and Tailwind CSS&lt;/li&gt;
&lt;li&gt;Light, dark, and system theme support&lt;/li&gt;
&lt;li&gt;Interactive sections: About, Benefits, Types, Recipes, FAQs, Events, and more&lt;/li&gt;
&lt;li&gt;Newsletter signup and event sidebar&lt;/li&gt;
&lt;li&gt;Downloadable recipe booklet (demo)&lt;/li&gt;
&lt;li&gt;Animated transitions and snackbars&lt;/li&gt;
&lt;li&gt;State management with Zustand&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Getting Started&lt;/h2&gt;
&lt;/div&gt;
&lt;div class="markdown-heading"&gt;
&lt;h3 class="heading-element"&gt;Prerequisites&lt;/h3&gt;

&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://nodejs.org/" rel="nofollow noopener noreferrer"&gt;Node.js&lt;/a&gt; (v18+ recommended)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.npmjs.com/" rel="nofollow noopener noreferrer"&gt;npm&lt;/a&gt; or &lt;a href="https://yarnpkg.com/" rel="nofollow noopener noreferrer"&gt;yarn&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="markdown-heading"&gt;
&lt;h3 class="heading-element"&gt;Installation&lt;/h3&gt;

&lt;/div&gt;
&lt;p&gt;Clone the repository and install dependencies:&lt;/p&gt;
&lt;div class="highlight highlight-source-shell notranslate position-relative overflow-auto js-code-highlight"&gt;
&lt;pre&gt;git clone https://github.com/your-username/goat-cheese-app.git
&lt;span class="pl-c1"&gt;cd&lt;/span&gt; goat-cheese-app
npm install&lt;/pre&gt;

&lt;/div&gt;
&lt;div class="markdown-heading"&gt;
&lt;h3 class="heading-element"&gt;Development&lt;/h3&gt;

&lt;/div&gt;
&lt;p&gt;Start the development server:&lt;/p&gt;
&lt;div class="highlight highlight-source-shell notranslate position-relative overflow-auto js-code-highlight"&gt;
&lt;pre&gt;npm run dev&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;Open &lt;a href="http://localhost:5173" rel="nofollow noopener noreferrer"&gt;http://localhost:5173&lt;/a&gt; in your browser.&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h3 class="heading-element"&gt;Build&lt;/h3&gt;

&lt;/div&gt;
&lt;p&gt;To build for production:&lt;/p&gt;
&lt;div class="highlight highlight-source-shell notranslate position-relative overflow-auto js-code-highlight"&gt;
&lt;pre&gt;npm run build&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;To preview the production build:&lt;/p&gt;
&lt;div class="highlight highlight-source-shell notranslate position-relative overflow-auto js-code-highlight"&gt;
&lt;pre&gt;npm run preview&lt;/pre&gt;

&lt;/div&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Project Structure&lt;/h2&gt;

&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;src/&lt;/code&gt; – Main source code (components, theme, stores, assets)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;public/&lt;/code&gt; –…&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/beretests/devto-challenge-june-celebrations-2025" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


&lt;h2&gt;
  
  
  Journey
&lt;/h2&gt;

&lt;p&gt;First off, I'd like to say a huge &lt;strong&gt;THANK YOU&lt;/strong&gt; to the dev.to team for putting this challenge together. When designing web pages, I tend to think more in terms of functionality than aesthetics so this was a great chance to flex my design skills 😉&lt;/p&gt;

&lt;p&gt;Landing page was built with React, Typescript, Vite and MUI. I originally planned to also use Taiwind for styling but I did a deep dive into the MUI Component APIs and found it much easier to do the component styling with those instead. I originally set up the project with MUI's &lt;a href="https://github.com/mui/material-ui/tree/master/examples/material-ui-vite-tailwind-ts" rel="noopener noreferrer"&gt;Vite example with Tailwind CSS in TypeScript&lt;/a&gt;. I started by planning the sections and user flow, then built reusable components for each feature (like FAQs, recipes, and events).&lt;/p&gt;

&lt;h3&gt;
  
  
  Learning Takeaways
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Create and apply custom themes using the &lt;a href="https://material-foundation.github.io/material-theme-builder/" rel="noopener noreferrer"&gt;MUI theme builder&lt;/a&gt;. One of the hardest things for me in web design is picking a color (or font) so this was really helpful.&lt;/li&gt;
&lt;li&gt;Separation of concerns: I tried to separate 'data' from code where possible as can be observed in the &lt;em&gt;components&lt;/em&gt; directory. I think it helps to make the code more readable.&lt;/li&gt;
&lt;li&gt;Use of Zustand for simple state management. Switching between themes is now a breeze and I am able to add as many themes as I want (pretty cool). It also facilitated the implementation of alerts (on newsletter subscription and recipe download) making the landing page very interactive.&lt;/li&gt;
&lt;li&gt;Use of Zustand stores to implement alerts using MUI snackbar. This will help make the code more extensible if more features that require in-app alerts are added.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Proud Moments
&lt;/h3&gt;

&lt;p&gt;I'm especially proud of the interactive animations (like the animated events and animated list of benefits of goat cheese) which I implemented using the Framer Motion library. I'm also proud of the theme switcher implemented with MUI theming.&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%2F94azbnyij5sihcbgd16a.gif" 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%2F94azbnyij5sihcbgd16a.gif" alt="Image description" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Next Steps
&lt;/h3&gt;

&lt;p&gt;I'd like to dive deeper into the following:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;more event interactivity&lt;/li&gt;
&lt;li&gt;improving accessibility&lt;/li&gt;
&lt;li&gt;adding tests to ensure maintainability of the code&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>devchallenge</category>
      <category>frontendchallenge</category>
      <category>css</category>
      <category>javascript</category>
    </item>
    <item>
      <title>Understanding Stock Trends: Insights from 10 Years of Data</title>
      <dc:creator>beretests</dc:creator>
      <pubDate>Tue, 04 Feb 2025 10:57:36 +0000</pubDate>
      <link>https://dev.to/beretests/understanding-stock-trends-insights-from-10-years-of-data-51hj</link>
      <guid>https://dev.to/beretests/understanding-stock-trends-insights-from-10-years-of-data-51hj</guid>
      <description>&lt;p&gt;I've always entertained the idea of actively investing in stocks (especially when I hear news of &lt;a href="https://www.ctvnews.ca/world/the-world-s-10-richest-people-got-a-record-us-64-billion-richer-from-trump-s-re-election-1.7101505" rel="noopener noreferrer"&gt;significant growth in the investments of big players after major world events&lt;/a&gt;). I find it overwhelming though, especially with so many companies to choose from and factors to consider before selecting stocks to invest in. &lt;/p&gt;

&lt;p&gt;I recently started doing some Data Analytics, Machine Learning and AI courses as part of the &lt;a href="https://m2mtechconnect.com/programs/datatalent/jobseekers" rel="noopener noreferrer"&gt;M2M DataTalent program&lt;/a&gt;. For my first capstone project, I analysed ten years (2012 - 2022) of daily stock data for major social media companies (Facebook (Meta), Twitter, Snapchat, Etsy, and Pinterest) to uncover valuable insights.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Data Source: &lt;a href="https://www.kaggle.com/datasets/prasertk/major-social-media-stock-prices-20122022/data" rel="noopener noreferrer"&gt;Kaggle&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Key Findings from the Analysis
&lt;/h3&gt;

&lt;h4&gt;
  
  
  1. Price Trends Over Time
&lt;/h4&gt;

&lt;p&gt;I tracked the adjusted closing prices of these stocks over the period using a comparative line chart. This visualized how the value of each stock evolved and highlighted significant moments of growth or decline.&lt;br&gt;
&lt;iframe src="https://codesandbox.io/embed/dy64k5?view=preview"&gt;
&lt;/iframe&gt;
&lt;br&gt;
&lt;strong&gt;Relevance:&lt;/strong&gt; &lt;em&gt;This shows which stocks have shown consistent growth and which have faced major challenges, useful information for investors to understand long-term stability.&lt;/em&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  2. Trading Volume Analysis
&lt;/h4&gt;

&lt;p&gt;I compared the trading volumes across stocks using stacked bar charts. Volume measures how actively a stock is being bought and sold — a key indicator of market interest.&lt;br&gt;
&lt;iframe src="https://codesandbox.io/embed/vqhjzh?view=preview"&gt;
&lt;/iframe&gt;
&lt;br&gt;
&lt;strong&gt;Relevance:&lt;/strong&gt; &lt;em&gt;High trading volumes may indicate investor confidence (or sometimes lack thereof) or reactions to major news, while low volumes could signal lack of interest or stability.&lt;/em&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  3. Correlation Between Stocks
&lt;/h4&gt;

&lt;p&gt;A correlation heatmap revealed how closely the stocks move in relation to each other which was to be expected given that they are all in the same market.&lt;br&gt;
&lt;iframe src="https://codesandbox.io/embed/zz2ql6?view=preview"&gt;
&lt;/iframe&gt;
&lt;br&gt;
Out of curiosity, I decided to also compare these stocks to a completely different investment (in this case, Gold) to observe their correlation. As expected, the correlation between gold and the other stocks though negative, was barely existent.&lt;br&gt;
&lt;iframe src="https://codesandbox.io/embed/c784ty?view=preview"&gt;
&lt;/iframe&gt;
&lt;br&gt;
&lt;em&gt;Gold Data Source: &lt;a href="https://www.kaggle.com/datasets/sid321axn/gold-price-prediction-dataset" rel="noopener noreferrer"&gt;Kaggle&lt;/a&gt;&lt;/em&gt;&lt;br&gt;
&lt;strong&gt;Relevance:&lt;/strong&gt; &lt;em&gt;Understanding these relationships would help investors diversify portfolios to manage risk better.&lt;/em&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  4. Daily Returns and Volatility
&lt;/h4&gt;

&lt;p&gt;I analyzed the daily returns, which is simply how much a stock’s price changes day to day, and mapped their distributions. &lt;br&gt;
&lt;iframe src="https://codesandbox.io/embed/cg8554?view=preview"&gt;
&lt;/iframe&gt;
&lt;br&gt;
This helped to assess:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Average returns (potential gains).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Variability (volatility or risk).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Extreme values (outliers that may indicate unusual events).&lt;br&gt;
&lt;iframe src="https://codesandbox.io/embed/pxjvg6?view=preview"&gt;
&lt;/iframe&gt;
&lt;br&gt;
&lt;strong&gt;Relevance:&lt;/strong&gt; &lt;em&gt;This gives a clear picture of what to expect from a stock — steady growth or erratic swings between growth and decline.&lt;/em&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  5. Cumulative Returns
&lt;/h4&gt;

&lt;p&gt;Analysis of the stock cumulative returns is relevant to show the total growth of the stock investments over time. This provides a long-term perspective.&lt;br&gt;
&lt;iframe src="https://codesandbox.io/embed/h86yzc?view=preview"&gt;
&lt;/iframe&gt;
&lt;br&gt;
&lt;strong&gt;Relevance:&lt;/strong&gt; &lt;em&gt;From the chart above, one can easily infer which stocks would have provided the best returns for investors who held their stocks for years.&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Conclusion
&lt;/h3&gt;

&lt;p&gt;The stock market is very complex, especially for novice investors like me. I now understand however (and I hope you do too), that with the right tools and insights, it becomes easier to navigate. By analyzing historical data and focusing on important metrics like price trends, volumes, and returns, one can start building a solid foundation in stock investment.&lt;/p&gt;

&lt;h4&gt;
  
  
  Addendum: The Tools Behind the Analysis
&lt;/h4&gt;

&lt;p&gt;To perform this analysis, I used a range of data science tools and techniques. Here are the tools used:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Python:&lt;/strong&gt; The programming language used for data cleaning, processing, and visualization.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;NumPy:&lt;/strong&gt; For handling numerical data and performing mathematical calculations.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Pandas:&lt;/strong&gt; A Python library that makes it easy to manipulate and analyze large datasets.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Bokeh:&lt;/strong&gt; Visualization tool used to create charts.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Jupyter Notebooks:&lt;/strong&gt; An interactive coding environment where the analysis was conducted and documented. The code used in this analysis can be viewed in &lt;a href="https://colab.research.google.com/drive/1QjaLxZwgIx3ZEI-8JGqNWTWuNnUUGA6E?usp=sharing" rel="noopener noreferrer"&gt;this notebook&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>datascience</category>
      <category>python</category>
      <category>socialmedia</category>
    </item>
    <item>
      <title>Running a Discord Bot on Raspberry Pi</title>
      <dc:creator>beretests</dc:creator>
      <pubDate>Tue, 01 Oct 2024 02:43:59 +0000</pubDate>
      <link>https://dev.to/beretests/running-a-discord-bot-on-raspberry-pi-4la4</link>
      <guid>https://dev.to/beretests/running-a-discord-bot-on-raspberry-pi-4la4</guid>
      <description>&lt;p&gt;Cover Photo by &lt;a href="https://unsplash.com/@danieltafjord?utm_content=creditCopyText&amp;amp;utm_medium=referral&amp;amp;utm_source=unsplash" rel="noopener noreferrer"&gt;Daniel Tafjord&lt;/a&gt; on &lt;a href="https://unsplash.com/photos/white-and-black-dice-on-white-surface-ShgFu6BqFnM?utm_content=creditCopyText&amp;amp;utm_medium=referral&amp;amp;utm_source=unsplash" rel="noopener noreferrer"&gt;Unsplash&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I recently completed a software engineering bootcamp, started working on LeetCode easy questions and felt it would help keep me accountable if I had a daily reminder to solve questions. I decided to implement this using a discord bot running on a 24 hour schedule (on my trusty raspberry pi, of course) which would do the following:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;go to a predefined databank of easy leetcode questions&lt;/li&gt;
&lt;li&gt;grab a question which has not been posted to the discord channel&lt;/li&gt;
&lt;li&gt;post the leetcode question as a thread in the discord channel (so you can easily add your solution)&lt;/li&gt;
&lt;li&gt;question is marked as &lt;code&gt;posted&lt;/code&gt; to avoid posting it to the channel again&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media.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%2Fpcnecepd2n5szp6l5hkx.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.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%2Fpcnecepd2n5szp6l5hkx.png" alt="Image description"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;I realize it may be easier to just go to LeetCode and solve a question a day but I got to learn a lot about Python and Discord with help from ChatGPT on this mini-project. This is also my first attempt at sketchnoting so please bear with lol&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.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%2F4o1e3qoye81n2g2qeybr.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.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%2F4o1e3qoye81n2g2qeybr.png" alt="Image description"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Setup
&lt;/h2&gt;

&lt;p&gt;1. Use python virtual environment&lt;br&gt;
2. Install dependencies&lt;br&gt;
3. Set up Leetcode easy questions database&lt;br&gt;
4. Set up environment variables&lt;br&gt;
5. Create Discord app&lt;br&gt;
6. Run the Bot!&lt;/p&gt;
&lt;h3&gt;
  
  
  1. Use python virtual environment
&lt;/h3&gt;

&lt;p&gt;I recommend the use of a python virtual environment because when I initially tested this on Ubuntu 24.04, I encountered the error below&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.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%2F5j52gpbj7szs0mxepomu.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.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%2F5j52gpbj7szs0mxepomu.png" alt="Image description"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Setting it up is relatively easy, just run the following commands and voila, you're in a python virtual environment!&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python3 &lt;span class="nt"&gt;-m&lt;/span&gt; venv ~/py_envs
&lt;span class="nb"&gt;ls&lt;/span&gt; ~/py_envs  &lt;span class="c"&gt;# to confirm the environment was created&lt;/span&gt;
&lt;span class="nb"&gt;source&lt;/span&gt; ~/py_envs/bin/activate
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. Install dependencies
&lt;/h3&gt;

&lt;p&gt;The following dependencies are required:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;AWS CLI&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Install AWS CLI by running the following:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-O&lt;/span&gt; &lt;span class="s1"&gt;'https://awscli.amazonaws.com/awscli-exe-linux-aarch64.zip'&lt;/span&gt;
unzip awscli-exe-linux-aarch64.zip 
&lt;span class="nb"&gt;sudo&lt;/span&gt; ./aws/install
aws &lt;span class="nt"&gt;--version&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then run &lt;code&gt;aws configure&lt;/code&gt; to add the required credentials. See &lt;a href="https://docs.aws.amazon.com/cli/v1/userguide/cli-chap-configure.html" rel="noopener noreferrer"&gt;Configure the AWS CLI&lt;/a&gt; doc.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;pip dependencies&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The following pip dependencies can be installed with a requirements file by running &lt;code&gt;pip install -r requirements.txt&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# requirements.txt&lt;/span&gt;

discord.py
&lt;span class="c"&gt;# must install this version of numpy to prevent conflict with&lt;/span&gt;
&lt;span class="c"&gt;# pandas, both of which are required by leetscrape&lt;/span&gt;
&lt;span class="nv"&gt;numpy&lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;1.26.4   
leetscrape
python-dotenv
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3. Set up leetcode easy questions database
&lt;/h3&gt;

&lt;p&gt;Leetscrape was vital for this step. To learn more about it, see the &lt;a href="https://pypi.org/project/leetscrape/" rel="noopener noreferrer"&gt;Leetscrape docs&lt;/a&gt;.&lt;br&gt;
I only want to work on leetcode easy questions (to me, they're even quite difficult) so I did the following:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;grab the list of all questions from leetcode using leetscrape and save list to csv
&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;leetscrape&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;GetQuestionsList&lt;/span&gt;

&lt;span class="n"&gt;ls&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;GetQuestionsList&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;ls&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;scrape&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="c1"&gt;# Scrape the list of questions
&lt;/span&gt;&lt;span class="n"&gt;ls&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;questions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;head&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="c1"&gt;# Get the list of questions
&lt;/span&gt;&lt;span class="n"&gt;ls&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_csv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;directory&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;path/to/csv/file&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;ul&gt;
&lt;li&gt;create an Amazon DynamoDB table and populate it with list of easy questions filtered from csv saved in previous step.
&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;csv&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;boto3&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;botocore.exceptions&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;BotoCoreError&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ClientError&lt;/span&gt;

&lt;span class="c1"&gt;# Initialize the DynamoDB client
&lt;/span&gt;&lt;span class="n"&gt;dynamodb&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;boto3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;resource&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;dynamodb&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;filter_and_format_csv_for_dynamodb&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;input_csv&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;

    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;input_csv&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;mode&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nb"&gt;file&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;csv_reader&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;csv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;DictReader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;file&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;csv_reader&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="c1"&gt;# Filter based on difficulty and paidOnly fields
&lt;/span&gt;            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;difficulty&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Easy&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;paidOnly&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;False&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;item&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;QID&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;N&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;QID&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;])},&lt;/span&gt;  
                    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;titleSlug&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;S&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;titleSlug&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]},&lt;/span&gt; 
                    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;topicTags&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;S&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;topicTags&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]},&lt;/span&gt;  
                    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;categorySlug&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;S&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;categorySlug&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]},&lt;/span&gt;  
                    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;posted&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;BOOL&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;  
                &lt;span class="p"&gt;}&lt;/span&gt;
                &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;upload_to_dynamodb&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;table_name&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;table&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;dynamodb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Table&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;table_name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;batch_writer&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;batch&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;batch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;put_item&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Item&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;QID&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;QID&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;N&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]),&lt;/span&gt;  
                    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;titleSlug&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;titleSlug&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;S&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
                    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;topicTags&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;topicTags&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;S&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
                    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;categorySlug&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;categorySlug&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;S&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
                    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;posted&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;posted&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;BOOL&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
                &lt;span class="p"&gt;})&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Data uploaded successfully to &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;table_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="nf"&gt;except &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BotoCoreError&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ClientError&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Error uploading data to DynamoDB: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;create_table&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;table&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;dynamodb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create_table&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;TableName&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;leetcode-easy-qs&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;KeySchema&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;
                &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;AttributeName&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;QID&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;KeyType&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;HASH&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;  &lt;span class="c1"&gt;# Partition key
&lt;/span&gt;                &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="n"&gt;AttributeDefinitions&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;
                &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;AttributeName&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;QID&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;AttributeType&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;N&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;  &lt;span class="c1"&gt;# Number type
&lt;/span&gt;                &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="n"&gt;ProvisionedThroughput&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;ReadCapacityUnits&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;WriteCapacityUnits&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="c1"&gt;# Wait until the table exists
&lt;/span&gt;        &lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_waiter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;table_exists&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;wait&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TableName&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;leetcode-easy-qs&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Table &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;table_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; created successfully!&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Error creating table: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Call function to create the table
&lt;/span&gt;&lt;span class="nf"&gt;create_table&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="c1"&gt;# Example usage
&lt;/span&gt;&lt;span class="n"&gt;input_csv&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;getql.pyquestions.csv&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;  &lt;span class="c1"&gt;# Your input CSV file
&lt;/span&gt;&lt;span class="n"&gt;table_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;leetcode-easy-qs&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;      &lt;span class="c1"&gt;# DynamoDB table name
&lt;/span&gt;
&lt;span class="c1"&gt;# Step 1: Filter and format the CSV data
&lt;/span&gt;&lt;span class="n"&gt;questions&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;filter_and_format_csv_for_dynamodb&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;input_csv&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Step 2: Upload data to DynamoDB
&lt;/span&gt;&lt;span class="nf"&gt;upload_to_dynamodb&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;questions&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;table_name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h3&gt;
  
  
  4. Set up environment variables
&lt;/h3&gt;

&lt;p&gt;Create a &lt;code&gt;.env&lt;/code&gt; file to store environment variables&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;DISCORD_BOT_TOKEN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;*****&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  5. Create Discord app
&lt;/h3&gt;

&lt;p&gt;Follow the instructions in the &lt;a href="https://discord.com/developers/docs/quick-start/getting-started" rel="noopener noreferrer"&gt;Discord Developer docs&lt;/a&gt; to create a Discord app and bot with adequate permissions. Be sure to authorize the bot with at least the following OAuth permissions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Send Messages&lt;/li&gt;
&lt;li&gt;Create Public Threads&lt;/li&gt;
&lt;li&gt;Send Messages in Threads&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  6. Run the Bot!
&lt;/h3&gt;

&lt;p&gt;Below is the code for the bot which can be run with the &lt;code&gt;python3 discord-leetcode-qs.py&lt;/code&gt; command.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;discord&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;boto3&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;leetscrape&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;GetQuestion&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;discord.ext&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;tasks&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;dotenv&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;load_dotenv&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;
&lt;span class="nf"&gt;load_dotenv&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="c1"&gt;# Discord bot token
&lt;/span&gt;&lt;span class="n"&gt;TOKEN&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;DISCORD_TOKEN&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Set the intents for the bot
&lt;/span&gt;&lt;span class="n"&gt;intents&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;discord&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Intents&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;default&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;intents&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;message_content&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt; &lt;span class="c1"&gt;# Ensure the bot can read messages
&lt;/span&gt;
&lt;span class="c1"&gt;# Initialize the bot
&lt;/span&gt;&lt;span class="n"&gt;bot&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;discord&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Client&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;intents&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;intents&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;# DynamoDB setup
&lt;/span&gt;&lt;span class="n"&gt;dynamodb&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;boto3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;client&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;dynamodb&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;TABLE_NAME&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;leetcode-easy-qs&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;
&lt;span class="n"&gt;CHANNEL_ID&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1211111111111111111&lt;/span&gt;  &lt;span class="c1"&gt;# Replace with the actual channel ID
&lt;/span&gt;
&lt;span class="c1"&gt;# Function to get the first unposted item from DynamoDB
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_unposted_item&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;dynamodb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;scan&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;TableName&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;TABLE_NAME&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;FilterExpression&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;posted = :val&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;ExpressionAttributeValues&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;:val&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;BOOL&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;}},&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;items&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Items&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[])&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;

&lt;span class="c1"&gt;# Function to mark the item as posted in DynamoDB
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;mark_as_posted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;qid&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;dynamodb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update_item&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;TableName&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;TABLE_NAME&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;Key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;QID&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;N&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;qid&lt;/span&gt;&lt;span class="p"&gt;)}},&lt;/span&gt;
        &lt;span class="n"&gt;UpdateExpression&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;SET posted = :val&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;ExpressionAttributeValues&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;:val&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;BOOL&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;}}&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;MAX_MESSAGE_LENGTH&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2000&lt;/span&gt;
&lt;span class="n"&gt;AUTO_ARCHIVE_DURATION&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2880&lt;/span&gt;

&lt;span class="c1"&gt;# Function to split a question into words by spaces or newlines
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;split_question&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;question&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max_length&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;parts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;question&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;max_length&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;split_at&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;question&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;rfind&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max_length&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;split_at&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;split_at&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;question&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;rfind&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max_length&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;split_at&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;split_at&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;max_length&lt;/span&gt;

        &lt;span class="n"&gt;parts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;question&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt;&lt;span class="n"&gt;split_at&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
        &lt;span class="c1"&gt;# Continue with the remaining text
&lt;/span&gt;        &lt;span class="n"&gt;question&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;question&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;split_at&lt;/span&gt;&lt;span class="p"&gt;:].&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;question&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;parts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;question&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;parts&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;clean_question&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;question&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;first_line&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;remaining_question&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;partition&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;\n{3,}&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;remaining_question&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;extract_first_line&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;question&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;lines&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;question&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;splitlines&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;lines&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;

&lt;span class="c1"&gt;# Task that runs on a schedule
&lt;/span&gt;&lt;span class="nd"&gt;@tasks.loop&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;minutes&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1440&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; 
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;scheduled_task&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;channel&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;bot&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_channel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;CHANNEL_ID&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;item&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;get_unposted_item&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;title_slug&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;titleSlug&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;S&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="n"&gt;qid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;QID&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;N&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="n"&gt;question&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;%s&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;GetQuestion&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;titleSlug&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;title_slug&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;scrape&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;

        &lt;span class="n"&gt;first_line&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;extract_first_line&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;question&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;cleaned_question&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;clean_message&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;question&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;parts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;split_message&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cleaned_question&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;MAX_MESSAGE_LENGTH&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="n"&gt;thread&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create_thread&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;first_line&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
            &lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;discord&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ChannelType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;public_thread&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;part&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;parts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;thread&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;part&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="nf"&gt;mark_as_posted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;qid&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;No unposted items found.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nd"&gt;@bot.event&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;on_ready&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;bot&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; has connected to Discord!&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;scheduled_task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;start&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="nd"&gt;@bot.event&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;on_thread_create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;thread&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;thread&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;Your challenge starts here! Good Luck!&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Run the bot
&lt;/span&gt;&lt;span class="n"&gt;bot&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TOKEN&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There are multiple options to run the bot. Right now, I'm just running this in a &lt;code&gt;tmux&lt;/code&gt; shell but you could also run this in a docker container or on a VPC from AWS, Azure, DigitalOcean or other cloud providers.&lt;/p&gt;

&lt;p&gt;Now I just have to actually attempt solving the Leetcode questions...&lt;/p&gt;

</description>
      <category>python</category>
      <category>raspberrypi</category>
      <category>discord</category>
      <category>aws</category>
    </item>
    <item>
      <title>Setting up a Gitlab Server on a Raspberry Pi 4</title>
      <dc:creator>beretests</dc:creator>
      <pubDate>Mon, 22 Apr 2024 23:05:41 +0000</pubDate>
      <link>https://dev.to/beretests/setting-up-a-gitlab-server-on-a-raspberry-pi-4-4ac7</link>
      <guid>https://dev.to/beretests/setting-up-a-gitlab-server-on-a-raspberry-pi-4-4ac7</guid>
      <description>&lt;h2&gt;
  
  
  Preamble
&lt;/h2&gt;

&lt;p&gt;My initial plan was to set up the GitLab server in docker on the Raspberry Pi mainly for portability as I would be setting up different tools and had no plans to add resources to the Pi. However, I ran into some problems. I'm not particularly equipped to resolve them now (believe me I tried) but I intend to retry this sometime in the future. Perhaps some of those issues would be resolved by then (such as an available GitLab docker image that runs on Pi 4 on 32-bit Debian/buster).&lt;/p&gt;

&lt;p&gt;Please read my &lt;a href="https://dev.to/beretests/the-start-of-my-devops-personal-projects-journey-38pa"&gt;initial post&lt;/a&gt; to understand why I chose Raspberry Pi as the server host. &lt;/p&gt;

&lt;h2&gt;
  
  
  Setup Steps
&lt;/h2&gt;

&lt;p&gt;The steps to complete the setup are outlined below. Expand each step (by clicking on it) for details.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Install screenshot on the Pi&lt;/strong&gt;&lt;br&gt;
I needed a way to take screenshots on the Pi to record the actions to complete the setup. I found the recommendations on &lt;a href="https://www.tomshardware.com/news/raspberry-pi-capture-scre&amp;lt;br&amp;gt;%0Aenshots,40276.html" rel="noopener noreferrer"&gt;this page&lt;/a&gt; helpful. I went with installing &lt;em&gt;Gnome Screenshot&lt;/em&gt;. It's not as intuitive as simply tapping Cmd + Shift +4 on a Mac but it does the job.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Network Setup&lt;/strong&gt;&lt;br&gt;
I'm on a standard home network with a simple router and the connecting devices - the Pi, a few laptops a TV, etc (you get the drift). The router assigns IP addresses to the devices via DHCP. All devices on the network have the same public IP address so I had to ensure that all requests to the GitLab server would go to the Pi. I'll outline the steps taken to achieve this.&lt;/p&gt;

&lt;p&gt;&lt;br&gt;
&lt;strong&gt;DISCLAIMER:&lt;/strong&gt; &lt;em&gt;This was a bit of head-scratcher for me given I'm no networking guru. It was a huge relief when I finally got it working. I also got to enhance my understanding of how networks work so yay!&lt;/em&gt;&lt;/p&gt;





&lt;ul&gt;
&lt;li&gt;&lt;em&gt;Edit DNS configuration on the router&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Given that the public IP address assigned by the router was not static, I needed to mitigate the issue of loss in connectivity to the server if/when it changed. Luckily, DNS for my hosts is managed in Cloudflare and they have &lt;a href="https://developers.cloudflare.com/dns/manage-dns-records/how-to/managing-dynamic-ip-addresses/" rel="noopener noreferrer"&gt;a few options&lt;/a&gt; for this.&lt;br&gt;
I chose to use DNS-O-Matic by going to the link from the Cloudflare page linked above and following the subsequent setup instructions.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;em&gt;Set the private IP address on the Pi to static&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The IP addresses assigned to the devices by the routers have set lease periods so the Raspberry Pi's IP address would change at given intervals. This was not ideal for my desired setup because I needed a stable way to direct traffic to it (the Pi). This was easily resolved by setting the private IP address to &lt;code&gt;fixed&lt;/code&gt; or &lt;code&gt;static&lt;/code&gt; in the router so that it never changes. Different routers have different ways to do this so I'll not provide details.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;em&gt;Enable port forwarding on the router&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;(&lt;em&gt;Welcome to the most head-scratchy part of this!&lt;/em&gt;)&lt;br&gt;
By default, the router is set up to block all incoming connections to the devices. It was necessary to enable port forwarding from the router to the Pi. In summary, this is opening one or two ports (I opened 2) on the router and directing all connections from that port to a port on the Pi. The exact way to do this varies from router to router so I won't get into detailed steps here but I found the following links useful (they are a little old but still useful, I found):&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.computerhope.com/issues/ch001201.htm" rel="noopener noreferrer"&gt;https://www.computerhope.com/issues/ch001201.htm&lt;/a&gt;&lt;br&gt;
&lt;a href="https://nordvpn.com/blog/open-ports-on-router/" rel="noopener noreferrer"&gt;https://nordvpn.com/blog/open-ports-on-router/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Install gitlab server accessible on HTTP&lt;/strong&gt;&lt;br&gt;
In the previous step, I did not open the standard ports 80 and 443 for HTTP and HTTPs because I still plan to install other services accessible on the web. This &lt;a href="https://en.wikipedia.org/wiki/List_of_TCP_and_UDP_port_numbers" rel="noopener noreferrer"&gt;Wikipedia article&lt;/a&gt; has a list of available ports - in total, there are 65,535. In addition (and I could be wrong here), using a different port provides an additional layer of security since bad actors typically send bad requests to known ports.&lt;br&gt;
&lt;br&gt;
&lt;strong&gt;FUN FACT:&lt;/strong&gt; &lt;em&gt;I once worked with an older engineer who was often miffed at how unimaginative the requests sent by bad actors were. He would be even more upset that those requests were sometimes successful implying that sysadmins were not doing enough to mitigate known vulnerabilities. I enjoyed working with him and learned a lot from him.&lt;/em&gt;&lt;br&gt;
&lt;br&gt;
To install the GitLab server, I followed the steps outlined in the &lt;a href="https://about.gitlab.com/install/#raspberry-pi-os" rel="noopener noreferrer"&gt;GitLab installation guide for the Raspberry Pi&lt;/a&gt; (thanks, GitLab for the easy-to-follow instructions). I skipped the step to install &lt;code&gt;postfix&lt;/code&gt; since I already had an account with Zoho mail.&lt;br&gt;
&lt;br&gt;
On the first few tries of running the installation, it failed because LetsEncrypt, which is enabled by default, could not create the required TLS certificates to run on HTTPS as I had set the &lt;code&gt;EXTERNAL_URL&lt;/code&gt; similar to &lt;code&gt;https.gitlab.&amp;lt;site-name&amp;gt;.com&lt;/code&gt;.&lt;br&gt;
&lt;br&gt;
This is when I decided to set the server as accessible on HTTP. The SSL configuration was then completed in the next step.&lt;br&gt;
Given the number of installs I ran, I created a simple bash script for subsequent reruns instead of entering single commands multiple times. &lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;

&lt;span class="c"&gt;#!/bin/bash&lt;/span&gt;

&lt;span class="c"&gt;# Install and configure the necessary dependencies&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt-get &lt;span class="nb"&gt;install &lt;/span&gt;curl openssh-server ca-certificates apt-transport-https perl
curl https://packages.gitlab.com/gpg.key | &lt;span class="nb"&gt;sudo tee&lt;/span&gt; /etc/apt/trusted.gpg.d/gitlab.asc

&lt;span class="c"&gt;# Add the GitLab package repository&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;curl &lt;span class="nt"&gt;-sS&lt;/span&gt; https://packages.gitlab.com/install/repositories/gitlab/raspberry-pi2/script.deb.sh | &lt;span class="nb"&gt;sudo &lt;/span&gt;bash

&lt;span class="c"&gt;#  install the GitLab package&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;&lt;span class="nv"&gt;EXTERNAL_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"http://gitlab.&amp;lt;site-name&amp;gt;.com:&amp;lt;port-number&amp;gt;"&lt;/span&gt; apt-get &lt;span class="nb"&gt;install &lt;/span&gt;gitlab-ce


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

&lt;/div&gt;

&lt;p&gt;I then made the file executable by running &lt;code&gt;sudo chmod +x filename&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The GitLab server was then successfully installed (finally!) with the following displayed: &lt;br&gt;
&lt;a href="https://media.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%2Fvku1wf6a132a2nqmjgjo.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.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%2Fvku1wf6a132a2nqmjgjo.png" alt="Screenshot of Success message of Gitlab server install"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A quick visit to the URL confirmed this and I could log in with the root password located at &lt;code&gt;/etc/gitlab/initial-root-password&lt;/code&gt;, and change that password via the &lt;a href="https://docs.gitlab.com/ee/security/reset_user_password.html#reset-your-root-password" rel="noopener noreferrer"&gt;provided instructions&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Setup acme-dns-certbot to run the server on HTTPS&lt;/strong&gt;&lt;br&gt;
To enable HTTPS and avoid opening port 80 to allow LetsEncrypt to create the required certificates, I opted to set LetsEncrypt to use the &lt;code&gt;dns-challenge&lt;/code&gt; type as opposed to the &lt;code&gt;http-challenge&lt;/code&gt; commonly used (more details on &lt;a href="https://letsencrypt.org/docs/challenge-types/" rel="noopener noreferrer"&gt;challenge types&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;I'm using Cloudflare to manage DNS and LetsEncrypt has detailed instructions on how to set this up so I won't go into details on how I did this but I'll add the links:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://snapcraft.io/docs/installing-snap-on-raspbian" rel="noopener noreferrer"&gt;Install snap&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://certbot.eff.org/instructions?ws=nginx&amp;amp;os=debianbuster&amp;amp;tab=wildcard" rel="noopener noreferrer"&gt;Install certbot&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://certbot-dns-cloudflare.readthedocs.io/en/stable/" rel="noopener noreferrer"&gt;certbot-dns-cloudflare setup&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;On completion of this setup and getting the certificates for my site, I then changed the following in the config file located at &lt;code&gt;/etc/gitlab/gitlab.rb&lt;/code&gt; &lt;/p&gt;

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

nginx['ssl_certificate'] = "&amp;lt;path-to-fullchain-cert&amp;gt;"
nginx['ssl_certificate_key'] = "&amp;lt;path-to-key&amp;gt;"
nginx['redirect_http_to_https'] = true
external_url = "https:gitlab.&amp;lt;site-name&amp;gt;.com:&amp;lt;port-number&amp;gt;"
letsencrypt['enable'] = false


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

&lt;/div&gt;

&lt;p&gt;I then ran &lt;code&gt;sudo gitlab-ctl reconfigure&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The server was then available at the new HTTPS URL (I had to wait a few minutes after the command run completed) which I confirmed in the screenshot below. Root login also worked as before.&lt;br&gt;
&lt;a href="https://media.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%2Fdoupekefu2dwum3voode.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.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%2Fdoupekefu2dwum3voode.png" alt="Image description"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Change SMTP Settings&lt;/strong&gt;&lt;br&gt;
This initial setup's final step was configuring SMTP since I skipped the &lt;code&gt;postfix&lt;/code&gt; install step during installation. As usual, the &lt;a href="https://docs.gitlab.com/omnibus/settings/smtp" rel="noopener noreferrer"&gt;documentation&lt;/a&gt; provides handy examples for this so I just changed the settings in &lt;code&gt;/etc/gitlab/gitlab.rb&lt;/code&gt; following the &lt;a href="https://docs.gitlab.com/omnibus/settings/smtp#zoho-mail" rel="noopener noreferrer"&gt;Zoho example&lt;/a&gt; in the docs and ran &lt;code&gt;sudo gitlab-ctl reconfigure&lt;/code&gt; again.&lt;/p&gt;

&lt;h2&gt;
  
  
  Next Steps
&lt;/h2&gt;

&lt;p&gt;There are several ways to configure the GitLab server as can be observed in the &lt;a href="https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/files/gitlab-config-template/gitlab.rb.template" rel="noopener noreferrer"&gt;config template&lt;/a&gt;. For the next step, I plan to configure it to store objects and uploads in Cloudflare's R2 (S3-compatible) storage which is currently free for up to 10G/month (please Cloudflare, don't make us start paying for this!). After that, who knows? I'll take each day as it comes and ensure I document the config changes as I go along.&lt;/p&gt;

&lt;h2&gt;
  
  
  Other Takeaways
&lt;/h2&gt;

&lt;p&gt;I found it was necessary to uninstall the GitLab server between installation failure attempts. I found helpful &lt;a href="https://www.sindastra.de/p/798/how-to-uninstall-gitlab" rel="noopener noreferrer"&gt;instructions&lt;/a&gt; which I put in a simple (and eventually executable) script.&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;

&lt;span class="c"&gt;#!/bin/bash&lt;/span&gt;

&lt;span class="c"&gt;# uninstall from gitlab-ctl&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;gitlab-ctl uninstall
&lt;span class="nb"&gt;sudo &lt;/span&gt;gitlab-ctl cleanse
&lt;span class="nb"&gt;sudo &lt;/span&gt;gitlab-ctl remove-accounts

&lt;span class="c"&gt;# uninstall with package manager &lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;dpkg &lt;span class="nt"&gt;-P&lt;/span&gt; gitlab-ce

&lt;span class="c"&gt;# delete associated directories and files&lt;/span&gt;
&lt;span class="nb"&gt;sudo rm&lt;/span&gt; &lt;span class="nt"&gt;-rf&lt;/span&gt; /opt/gitlab&lt;span class="k"&gt;*&lt;/span&gt;
&lt;span class="nb"&gt;sudo rm&lt;/span&gt; &lt;span class="nt"&gt;-rf&lt;/span&gt; /var/opt/gitlab&lt;span class="k"&gt;*&lt;/span&gt;
&lt;span class="nb"&gt;sudo rm&lt;/span&gt; &lt;span class="nt"&gt;-rf&lt;/span&gt; /etc/gitlab&lt;span class="k"&gt;*&lt;/span&gt;
&lt;span class="nb"&gt;sudo rm&lt;/span&gt; &lt;span class="nt"&gt;-rf&lt;/span&gt; /var/log/gitlab&lt;span class="k"&gt;*&lt;/span&gt;

&lt;span class="c"&gt;# remove the entries from your apt sources&lt;/span&gt;
&lt;span class="nb"&gt;sudo rm&lt;/span&gt; /etc/apt/sources.list.d/runner_gitlab-runner.list&lt;span class="k"&gt;*&lt;/span&gt;
&lt;span class="nb"&gt;sudo rm&lt;/span&gt; /etc/apt/sources.list.d/gitlab_gitlab-ee.list&lt;span class="k"&gt;*&lt;/span&gt;
&lt;span class="nb"&gt;sudo rm&lt;/span&gt; /etc/apt/sources.list.d/gitlab_gitlab-ce.list&lt;span class="k"&gt;*&lt;/span&gt;


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

&lt;/div&gt;

&lt;p&gt;In the course of uninstalling Gitlab, I encountered some issues with removing some &lt;code&gt;apt&lt;/code&gt; repos but this &lt;a href="https://raspberrypi.stackexchange.com/a/74255" rel="noopener noreferrer"&gt;StackOverflow answer&lt;/a&gt; helped me resolve that.&lt;/p&gt;

&lt;p&gt;I hope you find this useful. Feel free to leave a comment to let me know your thoughts, thanks for reading!&lt;/p&gt;

</description>
      <category>raspberrypi</category>
      <category>gitlab</category>
      <category>learning</category>
    </item>
    <item>
      <title>The Start of my Devops Personal Projects Journey</title>
      <dc:creator>beretests</dc:creator>
      <pubDate>Mon, 22 Apr 2024 23:04:19 +0000</pubDate>
      <link>https://dev.to/beretests/the-start-of-my-devops-personal-projects-journey-38pa</link>
      <guid>https://dev.to/beretests/the-start-of-my-devops-personal-projects-journey-38pa</guid>
      <description>&lt;h2&gt;
  
  
  Why am I doing this?
&lt;/h2&gt;

&lt;p&gt;I am a software tester - have been one for more than 3 years. Before that, I'd worked in IT support for several years. I started helping out with some DevOps tasks at work over a year ago when the main DevOps guy in our team quit. I have since discovered that I enjoy the challenge of working in DevOps, especially because I am constantly learning new things.&lt;/p&gt;

&lt;p&gt;Anyway, I decided to do some personal projects to improve my knowledge. Cue the &lt;a href="https://cloudresumechallenge.dev/docs/the-challenge/"&gt;CloudResumeChallenge&lt;/a&gt; which I think is one of the best things that could have happened to me (and the DevOps learning community at large). The challenge is well thought out and encompasses all the different aspects of DevOps, in my opinion.&lt;/p&gt;

&lt;p&gt;I started the challenge, got distracted with work and life stuff, and quit after about a month. I picked up the &lt;a href="https://cloudresumechallenge.dev/docs/extensions/kubernetes-challenge/"&gt;k8schallenge&lt;/a&gt; when it started in March 2024 but got distracted from that as well, (I promise I don't have attention difficulties, at least I don't think so).&lt;/p&gt;

&lt;p&gt;Anyway, I started thinking, that instead of doing such challenges, I could just document little things I do at work and in hobby projects that are related to DevOps, similar to collecting fieldstones as described by Gerald Weinberg in his book - &lt;strong&gt;Weinberg on Writing: The Fieldstone Method&lt;/strong&gt;. It could simply be that I had been overwhelmed. That led me here. I haven't given up on either of the challenges yet - I'm just taking the longer route to complete them (at least that's what I tell myself hehe).&lt;/p&gt;

&lt;p&gt;In the course of completing the cloud resume challenge, I received a $200 free credit to use with Azure as a new subscriber and let that lapse because...well, life. I did the same with DigitalOcean so when I decided to get back to it, I thought, do I have to pay to use these services now? I didn't want to (and didn't think I could even afford to, not in the long run) so I decided to start doing something (anything) on my PC.&lt;/p&gt;

&lt;p&gt;I then remembered that, in my first year with my previous company, I had received a Raspberry Pi 4 (I may not remember exactly how I got it but I know it was from the company, I still have the package addressed to the former CEO lol - it was likely a Christmas gift or a prize for winning a game). When I got it initially, I didn't know what to do with it (I was still working at the Service Desk at the time with little knowledge of software development) so I tried to use it to teach my kids about computers with some &lt;a href="https://projects.raspberrypi.org/en"&gt;raspberry projects&lt;/a&gt;. It was fun at first but their interest quickly fizzled out (they blame my teaching skills, the little buggers). So the Raspberry Pi went into a box for the next few years.&lt;/p&gt;

&lt;p&gt;Cue the lightbulb moment! I could use the Raspberry Pi as a server for pretty much anything. I learned that in the course of completing my DevOps tasks at work. I didn't need Azure, DigitalOcean, or any other cloud providers, not for my immediate needs. It was a personal project, I didn't need to pay anything. I could set up applications of all kinds, the world was my oyster, oh the joy! &lt;/p&gt;

&lt;p&gt;So that's how I came to be writing this! It's mostly a way to track my progress and stay accountable. I'll start with setting up a Gitlab server on the Raspberry Pi because I want to set up pipelines for other tasks - just one of the things I haven't gotten to do at work yet. Hopefully, I'll get ideas for other tasks as I go along.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Cover Photo by Katerina Holmes: &lt;a href="https://www.pexels.com/photo/crop-black-woman-taking-notes-from-laptop-5905852/"&gt;https://www.pexels.com/photo/crop-black-woman-taking-notes-from-laptop-5905852/&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>learning</category>
      <category>devops</category>
    </item>
  </channel>
</rss>
