<?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: Alexandre Donciu-Julin</title>
    <description>The latest articles on DEV Community by Alexandre Donciu-Julin (@alexdjulin).</description>
    <link>https://dev.to/alexdjulin</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%2F606499%2F653184a6-c303-47af-9955-6171530676f5.jpg</url>
      <title>DEV Community: Alexandre Donciu-Julin</title>
      <link>https://dev.to/alexdjulin</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/alexdjulin"/>
    <language>en</language>
    <item>
      <title>I asked ChatGPT to generate my Spotify playlists</title>
      <dc:creator>Alexandre Donciu-Julin</dc:creator>
      <pubDate>Wed, 22 Nov 2023 09:03:24 +0000</pubDate>
      <link>https://dev.to/alexdjulin/i-asked-chatgpt-to-generate-my-spotify-playlists-4c8l</link>
      <guid>https://dev.to/alexdjulin/i-asked-chatgpt-to-generate-my-spotify-playlists-4c8l</guid>
      <description>&lt;p&gt;&lt;em&gt;A command-line tool to generate Spotify playlists based on ChatGPT prompts.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/alexdjulin/spotify-playlist-generator"&gt;Github Project&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Project description
&lt;/h2&gt;

&lt;p&gt;The goal of this simple project was to learn how to interact with the &lt;a href="https://github.com/openai/openai-python"&gt;OpenAI&lt;/a&gt; API and get useful information from ChatGPT in a format I can easily manipulate (JSON dictionaries of artists and songs). On the other side, it uses the &lt;a href="https://spotipy.readthedocs.io/en/2.22.1/"&gt;Spotipy&lt;/a&gt; module to interact with Spotify and create playlists. Linked together, these two modules can help you create creative playlists from songs suggested by ChatGPT according to a given prompt (Example: "Peaceful songs to listen to when it's raining"). It's a great way to discover new tunes!&lt;/p&gt;

&lt;p&gt;This project is built upon a tutorial by &lt;a href="https://www.udemy.com/course/mastering-openai/"&gt;Colt Steele: Mastering OpenAI Python APIs&lt;/a&gt; that I highly recommend, as it covers the basics of how ChatGPT works and how to interact with it. On top of the automatic playlist creation mode covered by the tutorial, I created an interactive mode, which lets you decide which songs should be added and gives you the possibility to blacklist artists and songs. I completed the project with additional methods to play and print playlists.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;p&gt;To use this tool, you need both an OpenAI and a Spotify Developer account.&lt;/p&gt;

&lt;h3&gt;
  
  
  OpenAI
&lt;/h3&gt;

&lt;p&gt;It's the company that created and offers access to different ChatGPT models. Go to the &lt;a href="https://platform.openai.com/signup"&gt;OpenAI website&lt;/a&gt; and create an account. It comes with some free credit to start and test the API. See &lt;a href="https://platform.openai.com/docs/quickstart?context=python"&gt;documentation&lt;/a&gt; to generate your own API-Key.&lt;/p&gt;

&lt;h3&gt;
  
  
  Spotify
&lt;/h3&gt;

&lt;p&gt;Create a &lt;a href="https://www.spotify.com/us/signup"&gt;Spotify account&lt;/a&gt; if you don't already have one, it's free. After that, log in to &lt;a href="https://developer.spotify.com/"&gt;Spotify for Developers&lt;/a&gt; and create a project. In the project settings, you can generate the client-ID and -Secret needed to connect and send requests to Spotify. See the &lt;a href="https://developer.spotify.com/documentation/web-api/tutorials/getting-started"&gt;documentation&lt;/a&gt; for more info.&lt;/p&gt;

&lt;p&gt;Once you have your credentials to access both APIs, you can add them to &lt;a href="https://github.com/alexdjulin/spotify-playlist-generator/blob/main/playlist_generator.py"&gt;&lt;em&gt;playlist_generator.py&lt;/em&gt;&lt;/a&gt;. I am using the &lt;a href="https://pypi.org/project/keyring/"&gt;keyring&lt;/a&gt; module to store and retrieve mine, but you can use an .env file, store them in environment variables or just use them directly in the code. In this last case, be careful not to share your code with anyone!&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="c1"&gt;# Set openai key
&lt;/span&gt;&lt;span class="n"&gt;openai&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;api_key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;keyring&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_password&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;openai_key&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;main_key&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 spotify client id and secret
&lt;/span&gt;&lt;span class="n"&gt;client_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;keyring&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_password&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;spotify&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;client_ID&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;client_secret&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;keyring&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_password&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;spotify&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;client_secret&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;h3&gt;
  
  
  Spotify Desktop
&lt;/h3&gt;

&lt;p&gt;If you want to be able to play songs during the playlist creation (in the interactive mode for instance), you will need &lt;a href="https://www.spotify.com/us/download/"&gt;Spotify Desktop&lt;/a&gt; installed on your machine. If you want to skip this part, comment all calls to the method &lt;em&gt;play_song_in_spotify&lt;/em&gt; in &lt;a href="https://github.com/alexdjulin/spotify-playlist-generator/blob/main/playlist_generator.py"&gt;&lt;em&gt;playlist_generator.py&lt;/em&gt;&lt;/a&gt;, as it will not be able to ask your Spotify player to play the given songs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Installation
&lt;/h2&gt;

&lt;p&gt;Clone project and install required libraries (in a virtual environment for instance)&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="n"&gt;git&lt;/span&gt; &lt;span class="n"&gt;clone&lt;/span&gt; &lt;span class="n"&gt;https&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;//&lt;/span&gt;&lt;span class="n"&gt;github&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;com&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;alexdjulin&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;spotify&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;playlist&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;generator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;git&lt;/span&gt;
&lt;span class="n"&gt;python&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="n"&gt;venv&lt;/span&gt; &lt;span class="n"&gt;venv&lt;/span&gt;
&lt;span class="n"&gt;venv&lt;/span&gt;\&lt;span class="n"&gt;Scripts&lt;/span&gt;\&lt;span class="n"&gt;activate&lt;/span&gt; &lt;span class="c1"&gt;# on windows
&lt;/span&gt;&lt;span class="n"&gt;source&lt;/span&gt; &lt;span class="n"&gt;venv&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nb"&gt;bin&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;activate&lt;/span&gt; &lt;span class="c1"&gt;# on macOS/linux
&lt;/span&gt;&lt;span class="n"&gt;pip&lt;/span&gt; &lt;span class="n"&gt;install&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="n"&gt;requirements&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;txt&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  How to use it
&lt;/h2&gt;

&lt;p&gt;The most convenient way to start the playlist generator is to call &lt;a href="https://github.com/alexdjulin/spotify-playlist-generator/blob/main/main.py"&gt;&lt;em&gt;main.py&lt;/em&gt;&lt;/a&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;python main.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It will ask for the 4 required input variables:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Playlist Prompt&lt;/strong&gt; (-p):
A short description sent to ChatGPT to generate the playlist. Example: *Best songs for a road trip with your best friend.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Playlist Length&lt;/strong&gt; (-l):
How many songs should be in the playlist. Default value is 10.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Playlist Name&lt;/strong&gt; (-n):
The playlist name on Spotify. Optional, leave blank to use the prompt as a name. Example: &lt;em&gt;Road Trip Songs&lt;/em&gt;.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Interactive Mode&lt;/strong&gt; (-i):
Y to activate or N to deactivate the interactive mode. When active, each song will be played in Spotify and the user can choose to Add the song, blacklist it or blacklist the artist.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Playlist Prompt: Best songs for a road trip with your best friend
Playlist/Batch Length: 10
Playlist Name (leave blank to use prompt): Road Trip Songs
Interactive Mode? [y]es or [n]o: n
Creating playlist. Please wait...
&amp;gt;&amp;gt; end of playlist creation
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Additionally, you can call &lt;a href="https://github.com/alexdjulin/spotify-playlist-generator/blob/main/playlist_generator.py"&gt;&lt;em&gt;playlist_generator.py&lt;/em&gt;&lt;/a&gt; and pass these arguments in the command line directly. Don't forget the quotes around the strings. The interactive mode flag is TRUE if you pass -i, FALSE if you don't pass it:&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;# interactive flag off&lt;/span&gt;
python playlist_generator.py &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="s2"&gt;"Best songs for a road trip with your best friend"&lt;/span&gt; &lt;span class="nt"&gt;-l&lt;/span&gt; 10 &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="s2"&gt;"Road Trip Songs"&lt;/span&gt;
&lt;span class="c"&gt;# interactive flag on&lt;/span&gt;
python playlist_generator.py &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="s2"&gt;"Best songs for a road trip with your best friend"&lt;/span&gt; &lt;span class="nt"&gt;-l&lt;/span&gt; 10 &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="s2"&gt;"Road Trip Songs"&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Automatic Playlist Creation
&lt;/h2&gt;

&lt;p&gt;When the interactive mode is off, the playlist is created and filled automatically. It is great to create quick playlists, without worrying too much about the contents... Providing that you trust ChatGPT's suggestions, which can be sometimes surprising!&lt;/p&gt;

&lt;p&gt;Output when the interactive mode is off:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;----------------------------------------------------------------------------------------------------
------------------------------------ SPOTIFY PLAYLIST GENERATOR ------------------------------------
----------------------------------------------------------------------------------------------------
Prompt: Best songs for a road trip with your best friend
Length: 10
Name: Road Trip Songs
Interactive: False
----------------------------------------------------------------------------------------------------
1. Journey - Don't Stop Believin' | Escape (Bonus Track Version)
2. Smash Mouth - All Star | Astro Lounge
3. Bruce Springsteen - Born to Run | Born To Run
4. Guns N' Roses - Sweet Child O' Mine | Appetite For Destruction
5. John Denver - Take Me Home, Country Roads - Original Version | Poems, Prayers and Promises
6. ABBA - Dancing Queen | Arrival
7. Bon Jovi - Livin' On A Prayer | Slippery When Wet
8. Queen - Bohemian Rhapsody | Bohemian Rhapsody (The Original Soundtrack)
9. Lynyrd Skynyrd - Sweet Home Alabama | Second Helping (Expanded Edition)
10. Whitney Houston - I Wanna Dance with Somebody (Who Loves Me) | Whitney
----------------------------------------------------------------------------------------------------
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Spotify should open automatically, the songs are added to the playlist and the first one from the list should play when done.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--evfF4bq9--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://github.com/alexdjulin/spotify-playlist-generator/raw/main/readme/road_trip_automatic.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--evfF4bq9--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://github.com/alexdjulin/spotify-playlist-generator/raw/main/readme/road_trip_automatic.png" alt="" width="800" height="811"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Interactive Playlist Creation
&lt;/h2&gt;

&lt;p&gt;If the interactive flag is on, we get a first batch of suggestions from ChatGPT (using the input playlist length as batch length) and we play the songs one by one in Spotify. For each song, we have the choice to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Add the current song to the Playlist&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Blacklist this song (don't suggest it anymore)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Blacklist this artist (don't suggest any songs from this artist anymore)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Quit generating the playlist and keep it as it is&lt;br&gt;
&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Gary Jules - Mad World   (1/5)
[1] Add to Playlist
[2] Not this song
[3] Not this artist
[q] Quit playlist generation
Your choice:
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When the choice is done, we jump to the other song. When the batch is done, we get the following choice:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Ask ChatGPT for another batch of songs&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Quit generating the playlist and keep it as it is&lt;br&gt;
&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Do you want another batch of songs?
[1] Yes, give me more songs!
[2] No, I'm down with this playlist
Your choice:
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When asking ChatGPT for a new batch, we provide it with the list of songs we already have, as well as the lists of blacklisted songs and artists, to take into account for the next suggestions.&lt;/p&gt;

&lt;p&gt;Ouput when the interactive playlist is done:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;----------------------------------------------------------------------------------------------------
------------------------------------ SPOTIFY PLAYLIST GENERATOR ------------------------------------
----------------------------------------------------------------------------------------------------
Prompt: Lonely songs to play when it's raining
Length: 10
Name: Rainy Mood
Interactive: True
Artists Blacklist: {'The Carpenters', 'Willie Nelson'}
Songs Blacklist: {'Adore', "I Can't Make You Love Me"}
----------------------------------------------------------------------------------------------------
1. System Of A Down - Lonely Day | Hypnotize
2. a-ha - Crying in the Rain | East of the Sun, West of the Moon
3. Brook Benton - Rainy Night in Georgia | Brook Benton Today
4. Green Day - Boulevard of Broken Dreams | Boulevard of Broken Dreams
5. The Doors - Riders on the Storm | L.A. Woman
6. R.E.M. - Everybody Hurts | Automatic For The People
----------------------------------------------------------------------------------------------------
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--R0BS4bCV--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://github.com/alexdjulin/spotify-playlist-generator/raw/main/readme/rainy_mood_interactive.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--R0BS4bCV--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://github.com/alexdjulin/spotify-playlist-generator/raw/main/readme/rainy_mood_interactive.png" alt="" width="800" height="564"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;See GitHub &lt;a href="https://github.com/alexdjulin/spotify-playlist-generator/blob/main/README.md"&gt;readme&lt;/a&gt; for additional notes and contributions.&lt;/p&gt;

&lt;h1&gt;
  
  
  Conclusion
&lt;/h1&gt;

&lt;p&gt;This has been a fun little project and a good first experience playing around with the OpenAI API. The Spotipy library on the other side is very intuitive and you can quickly get around with it and interact with your Spotify account. Obviously, some prompts like "80's hits" will return you more accurate results than creative ones like "Sad songs to listen to when you just broke up and your fridge is empty". But I think switching the model to GPT4 and future models will probably improve on this.&lt;/p&gt;

</description>
      <category>python</category>
      <category>gpt3</category>
      <category>openai</category>
      <category>spotipy</category>
    </item>
    <item>
      <title>My Runing Map using python and folium</title>
      <dc:creator>Alexandre Donciu-Julin</dc:creator>
      <pubDate>Thu, 07 Sep 2023 20:27:13 +0000</pubDate>
      <link>https://dev.to/alexdjulin/my-runing-map-using-python-and-folium-2h2m</link>
      <guid>https://dev.to/alexdjulin/my-runing-map-using-python-and-folium-2h2m</guid>
      <description>&lt;p&gt;&lt;em&gt;A map of race events I took part in, with gpx traces and pop-up race information, entirely generated from a google spreadsheet.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://alexdjulin.ovh/run/run_map/run_map.html" rel="noopener noreferrer"&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%2F8z6tud27udf1ajls1byq.png" alt="race_map"&gt;&lt;/a&gt;&lt;em&gt;Click on map to display the interactive one.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/alexdjulin/running-events-map" rel="noopener noreferrer"&gt;Link to the Github project&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I'm a huge fan of maps, probably since I played around with my dad's roadmaps as a kid (he hated it, as he could never fold them back properly!). I cannot spend a day without fiddling&lt;br&gt;
with Google Maps. Although my trail running hobby and my GPS-watch addiction plays a big part in that!&lt;/p&gt;

&lt;p&gt;I have a running &lt;a href="https://run.alexdjulin.ovh/p/events.html" rel="noopener noreferrer"&gt;blog&lt;/a&gt; where I share reviews of events I took part in 🏃 But over the years, I kind of lost track of them. How many was there? Where did I run? How was the route? I need some kind of maps to visualise them. What if I could make it so simple, that I would only need to update a google spreadsheet with race information and the map on my blog would update accordingly? Let's see what we can do...&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;&lt;a href="https://python-visualization.github.io/folium/latest/" rel="noopener noreferrer"&gt;FOLIUM&lt;/a&gt;&lt;/strong&gt;. That's our code-word for today. Folium is a python module that is going to provide exactly what we need: Manipulate data in Python, then visualize it in a Leaflet map. &lt;/p&gt;

&lt;p&gt;Let's start with a simple example: Creating a map from one race event logged in this &lt;a href="https://docs.google.com/spreadsheets/d/1WghWJbxdCeKpbi3-H_6FmBAv_jILD_1_woRhuKGJ190/edit?usp=sharing" rel="noopener noreferrer"&gt;spreadsheet&lt;/a&gt;. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://docs.google.com/spreadsheets/d/1WghWJbxdCeKpbi3-H_6FmBAv_jILD_1_woRhuKGJ190/edit?usp=sharing" rel="noopener noreferrer"&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%2F229nw47uhsrv0k0xtr8j.png" alt="spreadsheet"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;See &lt;a href="https://github.com/alexdjulin/running-events-map/blob/main/tutorial/tutorial.py" rel="noopener noreferrer"&gt;tutorial.py&lt;/a&gt; on my git repo.&lt;/p&gt;




&lt;h2&gt;
  
  
  Download the spreadsheet
&lt;/h2&gt;

&lt;p&gt;Let's first download the google spreadsheet as a csv file, it will be easier to work with the data. I shared the document so anyone with the link can access it. The following command will download the spreadsheet.&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="n"&gt;sheet_url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;https://docs.google.com/spreadsheets/d/1WghWJbxdCeKpbi3-H_6FmBAv_jILD_1_woRhuKGJ190/export?exportFormat=csv&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;
&lt;span class="n"&gt;current_folder&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="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dirname&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;abspath&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;__file__&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="n"&gt;csv_filepath&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="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;current_folder&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;run_events.csv&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;command&lt;/span&gt; &lt;span class="o"&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;curl -L &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;sheet_url&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; -o &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;csv_filepath&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&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;system&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;command&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


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

&lt;/div&gt;

&lt;p&gt;You should now have a &lt;em&gt;run_events.csv&lt;/em&gt; file next to your script.&lt;/p&gt;




&lt;h2&gt;
  
  
  Load CSV data
&lt;/h2&gt;

&lt;p&gt;Who doesn't love &lt;a href="https://pandas.pydata.org/" rel="noopener noreferrer"&gt;Pandas&lt;/a&gt; 🐼? Load the contents of the csv file into a python dictionnary.&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;pandas&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;pd&lt;/span&gt;
&lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read_csv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;csv_filepath&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;to_dict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;orient&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;records&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="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Output:
# {'Date': '28.09.2014', 'Race': '41. Berlin Marathon', 'Latitude': 52.51625499, 'Longitude': 13.37757535, 'Time': '4:17:35', 'Link': 'https://www.bmw-berlin-marathon.com/'}
&lt;/span&gt;

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

&lt;/div&gt;




&lt;h2&gt;
  
  
  Create the html map
&lt;/h2&gt;

&lt;p&gt;Now we can start having fun! You will find Folium tutorials everywhere and the documentation itself is pretty straightforward. Let's generate a simple map and populate it with our data.&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;folium&lt;/span&gt;

&lt;span class="c1"&gt;# create map object and center it on our event
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;folium&lt;/span&gt;
&lt;span class="n"&gt;run_map&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;folium&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;location&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Latitude&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Longitude&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]],&lt;/span&gt; &lt;span class="n"&gt;tiles&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;zoom_start&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# add Openstreetmap layer
&lt;/span&gt;&lt;span class="n"&gt;folium&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;TileLayer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;openstreetmap&lt;/span&gt;&lt;span class="sh"&gt;'&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="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;OpenStreet Map&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;add_to&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;run_map&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# save and open map
&lt;/span&gt;&lt;span class="n"&gt;run_map&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;save&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;run_map.html&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;webbrowser&lt;/span&gt;
&lt;span class="n"&gt;webbrowser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;run_map.html&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;p&gt;Run the code, the map should be generated, saved and opened in your default web browser. As you can see, it's empty and just centered on the race location (Berlin). Click images to see html maps.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://alexdjulin.ovh/dev/run_map/map_empty.html" rel="noopener noreferrer"&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%2Fw9a0vgarq1et17y5bx7d.png" alt="map_empty"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Populate the map
&lt;/h2&gt;

&lt;p&gt;Let's add a marker point for our race to the map. We want it to be part of a 'Marathons' feature group, display the race name as tooltip, assign a color to it and display a short legend in the corner.&lt;/p&gt;

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

&lt;span class="c1"&gt;# add feature group for Marathons
&lt;/span&gt;&lt;span class="n"&gt;fg_marathons&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;folium&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;FeatureGroup&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="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Marathons&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;add_to&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;run_map&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# create marker and add it to marathon feature group
&lt;/span&gt;&lt;span class="n"&gt;folium_marker&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;folium&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Marker&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;location&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Latitude&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Longitude&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]],&lt;/span&gt; &lt;span class="n"&gt;tooltip&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Race&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;icon&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;folium&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Icon&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;color&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;red&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="n"&gt;folium_marker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_to&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fg_marathons&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# add legend in top right corner
&lt;/span&gt;&lt;span class="n"&gt;run_map&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_child&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;folium&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;LayerControl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;position&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;topright&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;collapsed&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;autoZIndex&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;


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

&lt;/div&gt;

&lt;p&gt;Our marker now shows up at the given coordinates. It displays the name of the race if we hover over it and we can display or hide it from the corner legend.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://alexdjulin.ovh/dev/run_map/map_marker.html" rel="noopener noreferrer"&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%2F8yqk62zjtj8tfa1fond7.png" alt="map_marker"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Add pop-up windows
&lt;/h2&gt;

&lt;p&gt;It would be nice to make that marker clickable and offer more information about the race. Let's create an html iframe that will pop-up when the user clicks on the marker. You will need some basic html knowledge for that, but nothing fancy at this point.&lt;/p&gt;

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

&lt;span class="c1"&gt;# create an iframe pop-up for the marker
&lt;/span&gt;&lt;span class="n"&gt;popup_html&lt;/span&gt; &lt;span class="o"&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;&amp;lt;b&amp;gt;Date:&amp;lt;/b&amp;gt; &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Date&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;br/&amp;gt;&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;popup_html&lt;/span&gt; &lt;span class="o"&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;&amp;lt;b&amp;gt;Race:&amp;lt;/b&amp;gt; &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Race&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;br/&amp;gt;&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;popup_html&lt;/span&gt; &lt;span class="o"&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;&amp;lt;b&amp;gt;Time:&amp;lt;/b&amp;gt; &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Time&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;br/&amp;gt;&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;popup_html&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;b&amp;gt;&amp;lt;a href=&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="s"&gt; target=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;_blank&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;gt;Event Page&amp;lt;/a&amp;gt;&amp;lt;/b&amp;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;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Link&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="n"&gt;popup_iframe&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;folium&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;IFrame&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;width&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;height&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;110&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;html&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;popup_html&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# modify the marker object to display the pop-up
&lt;/span&gt;&lt;span class="n"&gt;folium_marker&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;folium&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Marker&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;location&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Latitude&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Longitude&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]],&lt;/span&gt; &lt;span class="n"&gt;tooltip&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Race&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;popup&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;folium&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Popup&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;popup_iframe&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;icon&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;folium&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Icon&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;color&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;red&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="n"&gt;folium_marker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_to&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fg_marathons&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


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

&lt;/div&gt;

&lt;p&gt;There it is. We even created a link on the url, opening the event page in another tab. And since it's an html iframe, we can now basically display anything we want in it (pictures, videos, links, css stying, and so on).&lt;/p&gt;

&lt;p&gt;&lt;a href="https://alexdjulin.ovh/dev/run_map/map_popup.html" rel="noopener noreferrer"&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%2Fkfm0qe3aokx4cpl6kti3.png" alt="map_popup"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Add GPX trace
&lt;/h2&gt;

&lt;p&gt;Cherry on top, it would be amazing to display the route, like you usually see on the even't website.&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%2Fs80q7kkx6btlcva4sgv3.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%2Fs80q7kkx6btlcva4sgv3.png" alt="map_gpx"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This was actually easier than I thought. GPX files recorded by gps-watches or phones are XML-documents easy to read and parse. I used the &lt;a href="https://github.com/tkrajina/gpxpy" rel="noopener noreferrer"&gt;gpxpy&lt;/a&gt; library for that, which is doing exactly what we need: read the gpx file and extract points/segments from it to display on our map. You can find GPX files everywhere, on &lt;a href="https://www.strava.com/clubs/257389/group_events/680829" rel="noopener noreferrer"&gt;Strava&lt;/a&gt; or &lt;a href="https://www.komoot.com/tour/46519986" rel="noopener noreferrer"&gt;Komoot&lt;/a&gt; for instance.&lt;/p&gt;

&lt;p&gt;Here is how I opened and extracted segments from my own GPX file recorded during the race. I am using a &lt;em&gt;step&lt;/em&gt; value when slicing all the points, to smooth out the curve (loading 1 every 10 coordinate points).&lt;/p&gt;

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

&lt;span class="c1"&gt;# parse gpx file
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;gpxpy&lt;/span&gt;
&lt;span class="n"&gt;gpx_file&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;berlin_marathon_2014.gpx&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;
&lt;span class="n"&gt;gpx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;gpxpy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&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;gpx_file&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="n"&gt;track&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;gpx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tracks&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;segment&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;track&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;segments&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="c1"&gt;# load coordinate points
&lt;/span&gt;&lt;span class="n"&gt;points&lt;/span&gt; &lt;span class="o"&gt;=&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;track&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;gpx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tracks&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;segment&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;track&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;segments&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;step&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;point&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;segment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;points&lt;/span&gt;&lt;span class="p"&gt;[::&lt;/span&gt;&lt;span class="n"&gt;step&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
            &lt;span class="n"&gt;points&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="nf"&gt;tuple&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;point&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;latitude&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;point&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;longitude&lt;/span&gt;&lt;span class="p"&gt;]))&lt;/span&gt;

&lt;span class="c1"&gt;# add segments to the map
&lt;/span&gt;&lt;span class="n"&gt;folium_gpx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;folium&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;PolyLine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;points&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;color&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;red&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;weight&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;opacity&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.85&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;add_to&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;run_map&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# add the gpx trace to our marathon group
&lt;/span&gt;&lt;span class="n"&gt;folium_gpx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_to&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fg_marathons&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


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

&lt;/div&gt;

&lt;p&gt;Wonderful, the trace is showing up as expected on the map. Notice that, as we added it to the Marathons feature group, it inherits the red color and is affected by the legend checkbox too. You can now play around with the segments weight, opacity, color and with the step value to fine-tune it.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://alexdjulin.ovh/dev/run_map/map_gpx.html" rel="noopener noreferrer"&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%2Fttsolgc88jk6st65qpf3.png" alt="map_gpx"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That's it, you have all you need to build a kick-ass map. Just add multiple lines to the spreadsheet and modify your dataframe to load all the data into lists.&lt;/p&gt;




&lt;h2&gt;
  
  
  Improve the map
&lt;/h2&gt;

&lt;p&gt;If you browse through my &lt;a href="https://github.com/alexdjulin/running-events-map/blob/main/run_map.py" rel="noopener noreferrer"&gt;run_map.py&lt;/a&gt; script, you will notice that it is a bit more advanced. Here are some improvements I added to my map and to the project itself, to make it more appealing and to fit my needs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Add additional tile layers (ArcGIS)&lt;/li&gt;
&lt;li&gt;Group my events by type (halfs, marathons, ultras) and assign to each one a different color, also affecting the pop-up title and the gpx trace&lt;/li&gt;
&lt;li&gt;Customize the pop-up window with a picture of the race, a nice font, links, etc&lt;/li&gt;
&lt;li&gt;Move all paths and map settings to a json file, so there are no hard-coded values in the code and it is easier to change a setting (gpx trace weight or opacity for instance)&lt;/li&gt;
&lt;li&gt;Put all race events info into the google spreadsheet&lt;/li&gt;
&lt;li&gt;Add an ftp upload method at the end to send the html, jpg and gpx files onto the online storage my blog is using&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://alexdjulin.ovh/run/run_map/run_map.html" rel="noopener noreferrer"&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%2F8p46pf25dafs1gsrpbu2.png" alt="run_map_final"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You can see the final result in the &lt;a href="https://run.alexdjulin.ovh/p/events.html" rel="noopener noreferrer"&gt;Events&lt;/a&gt; tab of my running blog.&lt;/p&gt;

&lt;p&gt;Finally, you may notice that my script does not only update the map but also a table of events information displayed below it, as well as a little event-o-meter gadget in the sidebar. Both are generated when I run the script and updated according to the updated google spreadsheet information.&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%2F0z9yc5bzydg8px25v6an.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%2F0z9yc5bzydg8px25v6an.png" alt="event_table"&gt;&lt;/a&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%2Fzn5yams3l1cbigt2iusr.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%2Fzn5yams3l1cbigt2iusr.png" alt="eventometer"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;I therefore succeeded in my holy quest to put all my running events on a pretty nice-looking map. All I need to do now, after completing a new event, is to fill in the information in the google doc and provide a jpg thumbnail and the gpx trace of my run, then run the script to generate a new map and update the table and gadget. This last step could be automated of course, if we had our script running in the cloud and checking any update done on the spreadsheet.&lt;/p&gt;

&lt;p&gt;Have fun playing with folium and don't forget to share your maps!&lt;br&gt;
Take care and see you on the trail 🏔️🏃‍♂️&lt;/p&gt;

</description>
      <category>python</category>
      <category>beginners</category>
    </item>
    <item>
      <title>Draw and email your Secret Santas using Python and SendGrid</title>
      <dc:creator>Alexandre Donciu-Julin</dc:creator>
      <pubDate>Tue, 14 Dec 2021 09:23:41 +0000</pubDate>
      <link>https://dev.to/alexdjulin/draw-and-email-your-secret-santas-using-python-and-sendgrid-1d6b</link>
      <guid>https://dev.to/alexdjulin/draw-and-email-your-secret-santas-using-python-and-sendgrid-1d6b</guid>
      <description>&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--TjNFFQVz--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_66%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/b2yni5jhq1489qkrarad.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--TjNFFQVz--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_66%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/b2yni5jhq1489qkrarad.gif" alt="Chandler Santa" width="480" height="271"&gt;&lt;/a&gt;&lt;br&gt;
Project on GitHub &lt;a href="https://github.com/alexdjulin/secret-santa"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;It's that time of the year, AGAIN! It's been a fast one, but I do feel like I learned a lot, on my holy quest to become a rock-star programmer. That say, I would like to finish the year on a fun simple project. So when my mom asked me to draw our Secret Santas this year, I had it.&lt;/p&gt;

&lt;p&gt;If you are not familiar with the concept, it's a nice way to celebrate xmas amongst a large group of friends or family members, withoug having to spend a salary on gifs. Every member of the group is assigned one recipient, and as his/her Secret Santa (since these random assignments are kept secret), you need to buy a gift for this person only. This way everyone will get a nice present instead of dozens of cheap ones.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--6Xp65pF9--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_66%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/sfg479fbv54zv14s88fy.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--6Xp65pF9--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_66%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/sfg479fbv54zv14s88fy.gif" alt="Toilet Seats cover" width="500" height="375"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In the past years, I used a basic random name generator for that, but with the downside that I knew all the assignments and who my Secret Santa was. So this year, let's be clever and use a python script to do the draw and contact Santas per email.&lt;/p&gt;

&lt;p&gt;&lt;br&gt;__&lt;/p&gt;
&lt;h2&gt;
  
  
  Get list of Santas from a CSV file
&lt;/h2&gt;

&lt;p&gt;I leaned the Pandas library this year, which is great for extracting information from a CSV file. It's a user-friendly way to input data, easier than writing in the script directly. Here is our CSV file, with our six friends and the information we need to contact them.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--TE0IdtcG--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/17nrx833ug3uq56ckb7i.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--TE0IdtcG--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/17nrx833ug3uq56ckb7i.jpg" alt="CSV file" width="657" height="259"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;br&gt;__&lt;/p&gt;
&lt;h2&gt;
  
  
  The Black List
&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;Black list, quid est?&lt;/em&gt;&lt;br&gt;
I like to spice things up and I thought it would be handy to add a black list feature, i.e. a list of names that one Santa should not get for any reason. In our case for instance:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Ross and Rachel should not get each other, cause they had a big fight and they are 'on a break'&lt;/li&gt;
&lt;li&gt;Monica and Chandler should not get each other, as they are married and they wanna give each others more expensive presents&lt;/li&gt;
&lt;li&gt;Joey is broke, he bought a cheap present but it's for a guy so he cannot be one of the girl's Secret Santa&lt;/li&gt;
&lt;li&gt;Phoebe likes everyone so her black list is empty&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--9r6e_k8s--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_66%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/gqqeh4p88f3mmlq3mm9f.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--9r6e_k8s--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_66%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/gqqeh4p88f3mmlq3mm9f.gif" alt="We were on a break" width="480" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;br&gt;__&lt;/p&gt;
&lt;h2&gt;
  
  
  Create the SecretSanta class
&lt;/h2&gt;

&lt;p&gt;I learned programming using C++ and I have the CLASS word inprinted in caps inside my brain. So why not organise our script a bit by adding a &lt;em&gt;SecretSanta&lt;/em&gt; class? Here are the class members and methods we need for the draw:&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="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;SecretSanta&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;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;black_list&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="s"&gt;""" initialize class variables """&lt;/span&gt;
        &lt;span class="bp"&gt;self&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;name&lt;/span&gt;
        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt; 
        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;recipient&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;  &lt;span class="c1"&gt;# will be allocated later
&lt;/span&gt;        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;black_list&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;black_list&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# adding own name
&lt;/span&gt;        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;black_list&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;black_list&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;black_list&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&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;__repr__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="s"&gt;""" override print method (optional) """&lt;/span&gt;
        &lt;span class="c1"&gt;# return string to print
&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;contact_secret_santa&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="s"&gt;""" contact recipient """&lt;/span&gt;
        &lt;span class="c1"&gt;# use SendGrid example here
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;br&gt;__&lt;/p&gt;

&lt;h2&gt;
  
  
  Load settings from a json file
&lt;/h2&gt;

&lt;p&gt;Again, I find it more friendly to use external files to load variables instead of editing the code directly. So I usually add a &lt;strong&gt;settings.json&lt;/strong&gt; file to all my projects. Here are the variables that we need to define there:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Path to our CSV file containing Secret Santas information&lt;/li&gt;
&lt;li&gt;Path to the email template (.txt or .html)&lt;/li&gt;
&lt;li&gt;Max attempts that should be done while trying to assign recipients to secret santas. Due to the black lists, an assignment may not be possible and therefore this variable will break the while loop and raise an error.&lt;/li&gt;
&lt;li&gt;Email of your SendGrid account&lt;/li&gt;
&lt;li&gt;Personal SendGrid API key (private, don't share it with ANYONE)
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"csv_file"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"data/secret_santas_list.csv"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"email_file"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"data/email.html"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"attempts_limit"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"sg_sender_email"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"youremail@domain.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"sg_api_key"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"your_api_key"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The json library helps you import and store these parameters in a SETTINGS dictionnary, that you can use further in your script.&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="nn"&gt;json&lt;/span&gt;
&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nb"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"data/settings.json"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;'r'&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;json_file&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  
    &lt;span class="n"&gt;SETTINGS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;json_file&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SETTINGS&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;'csv_file'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="c1"&gt;# output: 'data/secret_santas_list.csv'
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;br&gt;__&lt;/p&gt;

&lt;h2&gt;
  
  
  Main - Create Santas
&lt;/h2&gt;

&lt;p&gt;We now have everything we need to write our main procedure.&lt;/p&gt;

&lt;p&gt;To extract the CSV infos into a Pandas dataframe and create our SecretSanta instances is a child game.&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;from&lt;/span&gt; &lt;span class="nn"&gt;pandas&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;read_csv&lt;/span&gt;
&lt;span class="n"&gt;df&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;read_csv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;csv_file_path&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;fillna&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;''&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;secret_santas&lt;/span&gt; &lt;span class="o"&gt;=&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;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;)):&lt;/span&gt;
    &lt;span class="n"&gt;new_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;'Name'&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="n"&gt;new_email&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;'Email'&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="n"&gt;new_black_list&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;'Black List'&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="n"&gt;new_santa&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;SecretSanta&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;new_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;new_email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;black_list&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;new_black_list&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;secret_santas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;new_santa&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;br&gt;__&lt;/p&gt;

&lt;h2&gt;
  
  
  Main - Draw
&lt;/h2&gt;

&lt;p&gt;Finally we can draw our Secret Santas!&lt;/p&gt;

&lt;p&gt;Since we introduced the black-list option, we face the issue that a draw might not be possible due to too many constraints (for instance, some poor guy who would be on everyone's black list!). I'm sure we could come up with a clever algorithm that would analyse all black-lists first and determine if a draw is possible or not. But let's keep it simple and take the easy road of trying, until we succeed or decide it's not gonna happen!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--jJeYVTaz--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_66%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/92jxdslxv1bis1euc1o0.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--jJeYVTaz--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_66%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/92jxdslxv1bis1euc1o0.gif" alt="Superman and Santa" width="480" height="278"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We will therefore wrap everything inside a while loop, that will break if the draw is successful (everyone has been assigned as Secret Santa) or after a maximum numner of attempts (defined in our settings file).&lt;/p&gt;

&lt;p&gt;Here are the main steps I am following, check the &lt;a href="https://github.com/alexdjulin/secret-santa"&gt;GitHub project&lt;/a&gt; for the (well-commented!) code:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Initiate the while loop with our two conditions (success or reaching max attempts)&lt;/li&gt;
&lt;li&gt;Shuffle our list of names and delete any previous assignment&lt;/li&gt;
&lt;li&gt;Go through our list of SecretSanta instances and look for a recipient, which is not on the black-list. If we find one, we assign it and we jump to the next SecretSanta. If not, the draw fails and we start a new one (the while loop restart, we are back at step 2)&lt;/li&gt;
&lt;li&gt;If we assign a recipient to all Secret Santas, the draw is successful and the while loop will break. If it never happens, the loop will break once reaching the max attemps.&lt;/li&gt;
&lt;li&gt;Process the result: A succesful draw will trigger contacting the Secret Santas, while a failed one will raise an error and ask the user to review the input parameters.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;br&gt;__&lt;/p&gt;

&lt;h2&gt;
  
  
  E-mailing Santas
&lt;/h2&gt;

&lt;p&gt;This is our last step. We are going to use &lt;a href="https://sendgrid.com/"&gt;Twilio SendGrid&lt;/a&gt; for this, which provides a cloud-based service that assists businesses with email delivery. This is a very easy step thanks to their crystal-clear tutorials. Here are the steps you need to follow to use SendGrid services.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://sendgrid.com/free/"&gt;Create a SendGrid account&lt;/a&gt; &lt;br&gt;
You need to register for an account first. The free offer allows you to send up to 100 emails a day, which is good enough for our little project.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://docs.sendgrid.com/ui/sending-email/sender-verification"&gt;Create a Single Sender Verification&lt;/a&gt;&lt;br&gt;
Your recipients will receive emails from a specific address. Terefore this e-mail needs to be existing and verified as your own.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://docs.sendgrid.com/ui/account-and-settings/api-keys"&gt;Create a personal API key&lt;/a&gt; &lt;br&gt;
This is an important step. The API key is personal and should never be shared. It will serve as a badass password (seriously, just look at it!) that will need to be passed everytime e-mails are sent from your account. For simplicity, you can add it to your environment variables.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/sendgrid/sendgrid-python"&gt;Install the SendGrid python library&lt;/a&gt;&lt;br&gt;
Finally you can install the python library. The documentation gives you some simple examples on how to send your first emails. I did not need to dig much further, those are great. This project uses the &lt;em&gt;"Without Mail Helper Class"&lt;/em&gt; example, that I implemented in my &lt;em&gt;contact_secret_santa()&lt;/em&gt; class method.&lt;/p&gt;

&lt;p&gt;&lt;br&gt;__&lt;/p&gt;

&lt;h2&gt;
  
  
  Personalising the email content
&lt;/h2&gt;

&lt;p&gt;This is an optional but nice little step. I found it more joyful and christmassy to write the email contents as from the hand of Santa himself, asking people to help him spread the joy. Sendgrid let you use a text or html format for the contents of the email. So why not draft an html file with some pictures, colors, a handwriting font, etc? You can also put tags like [NAME] and [RECIPIENT] that will be replaced with your class attributes to address the person directly. Put some CSS to make it look more personal and believable. I don't know much about web dev but feel free to check my html template.&lt;/p&gt;

&lt;p&gt;Once all these steps are done and if your draw was successful, you can now call the &lt;em&gt;contact_secret_santa()&lt;/em&gt; method of all your instances. They will be notified within a few minutes and receive an email based on your template. Hey, you got one too!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--hY64UqHH--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_66%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/bqlkhuuialzgrlu73hfw.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--hY64UqHH--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_66%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/bqlkhuuialzgrlu73hfw.gif" alt="Email contents" width="800" height="1058"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;br&gt;__&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;I hope this little project taught you something new, whether you are playing with CSV and json files, trying to send emails or simply improving your python skills. It was a fun last projet to wrap up this year, which has been for me rich in learning new stuff and hopefully a few more steps towards my goals.&lt;/p&gt;

&lt;p&gt;I wish you all a wonderful Christmas. Have a great Secret Santa sharing time with the ones you love (and the ones from your black list too!). Take care and read you next year :)&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--YRkmete6--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_66%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/7k5wk122bp6kh7rlvu8p.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--YRkmete6--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_66%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/7k5wk122bp6kh7rlvu8p.gif" alt="merry xmas" width="500" height="200"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;br&gt;__&lt;br&gt;
Friends pictures © Warner Bros. Entertainment Inc&lt;br&gt;
Cover picture from &lt;a href="https://www.rachelmiddleton.co.uk/"&gt;Rachel Middleton&lt;/a&gt;&lt;/p&gt;

</description>
      <category>python</category>
      <category>beginners</category>
      <category>webdev</category>
      <category>programming</category>
    </item>
    <item>
      <title>Live Link Face to Unreal MetaHuman Retarget</title>
      <dc:creator>Alexandre Donciu-Julin</dc:creator>
      <pubDate>Wed, 03 Nov 2021 07:41:04 +0000</pubDate>
      <link>https://dev.to/alexdjulin/live-link-face-to-unreal-metahuman-retarget-5f9b</link>
      <guid>https://dev.to/alexdjulin/live-link-face-to-unreal-metahuman-retarget-5f9b</guid>
      <description>&lt;p&gt;&lt;em&gt;How to retarget facial animations recorded with the Live Link Face iPhone app onto an Unreal MetaHuman character, using MotionBuilder and Python.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;You can find the project files on my &lt;a href="https://github.com/alexdjulin/LiveLinkFace-CSV-Retarget-For-Motionbuilder"&gt;Github repo&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--ZamlBfVZ--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_66%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/8o6l798yr0fhxyrjfkdy.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--ZamlBfVZ--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_66%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/8o6l798yr0fhxyrjfkdy.gif" alt="anim file retarget" width="450" height="331"&gt;&lt;/a&gt;&lt;br&gt;
&lt;br&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  About MetaHumans
&lt;/h2&gt;

&lt;p&gt;I recently started playing around with the mind-blowing MetaHuman &lt;em&gt;(MH)&lt;/em&gt; feature from Unreal Engine. Through the &lt;a href="https://quixel.com/"&gt;Quixel Bridge&lt;/a&gt; editor, you can create and customize a highly realistic avatar in a few clicks, and send it to your Unreal project. Everything is fine-tuned to get the best of Unreal shaders and will leave you speechless 😍 I invite you to take a peek at it, plenty of resources out there.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--9spihC2H--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/enycqzafiil9b9r10xia.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--9spihC2H--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/enycqzafiil9b9r10xia.jpg" alt="MetaHuman Editor" width="800" height="453"&gt;&lt;/a&gt;&lt;br&gt;
&lt;br&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  About Live Link Face
&lt;/h2&gt;

&lt;p&gt;As a mocap director, my interest quickly shifted over the live-retarget capabilities, i.e. how to transfer an actor's facial performance onto a metahuman, in real-time. Epic offers an app for that, which takes advantage of the iPhone's TrueDepth camera: &lt;a href="https://apps.apple.com/us/app/live-link-face/id1495370836"&gt;Live Link Face&lt;/a&gt; &lt;em&gt;(LLF)&lt;/em&gt;. It also takes a few click to link your phone to your Unreal project and start driving your MetaHuman's face. Again, it's working 'Epic-ly' fast and well!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--jwQzxlnD--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/zdh428kxgllv8vggcq7a.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--jwQzxlnD--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/zdh428kxgllv8vggcq7a.jpg" alt="Live Link Face" width="300" height="154"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;But there is the snag... The app was clearly designed to work in real-time with Unreal, where you can use the Sequencer to record facial data along body mocap, blueprints functions and so on. However, if used as a stand-alone app, it's another story. &lt;/p&gt;

&lt;p&gt;LLF saves for each take 4 files:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A CSV file containing the animation&lt;/li&gt;
&lt;li&gt;A video file of the performance&lt;/li&gt;
&lt;li&gt;A json file containing some technical information&lt;/li&gt;
&lt;li&gt;A jpg thumbnail
&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--yH56we-t--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/q4p3t33487eg2hvdnmin.jpg" alt="LLF Export" width="465" height="138"&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;No maya scene, no unreal asset, not even a simple fbx file that we could use to import and clean the motion in a 3rd-party-software like MotionBuilder. Just a CSV file filled with random blendshape names and millions of values. Like that zucchini-peeler you got from aunt Edna for xmas, it's a nice gesture but what the heck am I gonna do with that!? 🤔&lt;/p&gt;

&lt;p&gt;A quick look into the &lt;a href="https://docs.unrealengine.com/4.27/en-US/AnimatingObjects/SkeletalMeshAnimation/FacialRecordingiPhone/"&gt;documentation&lt;/a&gt; confirmed the idea:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;This data file is not currently used by Unreal Engine or by the Live Link Face app. However, the raw data in this file may be useful for developers who want to build additional tools around the facial capture.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;So, I guess we are on our own now!&lt;br&gt;
&lt;br&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Let's dig in
&lt;/h2&gt;

&lt;p&gt;It's not the most convenient format, but if you take a good look at this CSV file, it's all there: For each timecode frame (first column), you get a value of all the shapes defining the facial expression at that frame. These shapes come from Apple's &lt;a href="https://developer.apple.com/documentation/arkit"&gt;ARKit Framework&lt;/a&gt;, which uses the iphone's TrueDepth camera to detect facial expressions and generate blendshape coefficients. All we have to do is extract those time/value pairs and store then in some handy container. This is where the &lt;a href="https://pandas.pydata.org/"&gt;Pandas&lt;/a&gt; library 🐼 and its dataframes are gonna do wonders!&lt;br&gt;
&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--G5HzacFj--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/0vc1fw8q9m2y9ass0lgs.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--G5HzacFj--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/0vc1fw8q9m2y9ass0lgs.jpg" alt="CSV contents" width="547" height="299"&gt;&lt;/a&gt;&lt;br&gt;
&lt;br&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Now what should I do with all this?
&lt;/h2&gt;

&lt;p&gt;I need to recopy the animation values onto a target model that Unreal can import and read. Let's have a look at the &lt;a href="https://www.unrealengine.com/marketplace/en-US/product/metahumans"&gt;MetaHuman project&lt;/a&gt;, that you can find on the UE Marketplace.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--J55jAbBm--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/6hbkmyu11x3e3lkvpu8l.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--J55jAbBm--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/6hbkmyu11x3e3lkvpu8l.jpg" alt="MetaHuman project" width="660" height="279"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Facial and body motions are handled separately. A blueprint is applied to a facial version of the skeleton, which contains only the root, spine, neck, head and numerous facial joints, as Unreal relies on joint-based motions.&lt;br&gt;
&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--NJJrM3CX--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/mt2fso6wij7g1a4m32hg.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--NJJrM3CX--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/mt2fso6wij7g1a4m32hg.jpg" alt="Export MH skeleton" width="660" height="376"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I can get this skeleton out of Unreal by creating an animation pose and exporting it as FBX. Now we can open it in motionbuilder and... &lt;strong&gt;HOLY MOLY GUACAMOLE&lt;/strong&gt;, what's all this!? 😵 I'm not talking about the Hellraiser facial rig but about the root joint properties. It contains a huge amount of custom properties!&lt;br&gt;
&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--ZFu8BZ0u--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_66%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/sul5m6h6v9wq6tr0utnc.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--ZFu8BZ0u--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_66%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/sul5m6h6v9wq6tr0utnc.gif" alt="root properties" width="660" height="414"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I run a quick line of code and count about 1100 entries:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;254 properties starting with &lt;strong&gt;CTRL_expressions_&lt;/strong&gt;, which are clearly blendshapes, like &lt;em&gt;CTRL_expressions_jawOpen&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;757 properties starting with &lt;strong&gt;head_&lt;/strong&gt;, which seem to be additional correctives (fixing for instance a collision between two blendshapes activated at the same time).&lt;/li&gt;
&lt;li&gt;35 properties starting with &lt;strong&gt;cartilage_&lt;/strong&gt;, &lt;strong&gt;eyeLeft_&lt;/strong&gt;, &lt;strong&gt;eyeRight_&lt;/strong&gt; or &lt;strong&gt;teeth_&lt;/strong&gt;, which are probably more correctives?&lt;/li&gt;
&lt;li&gt;Last but not least: 53 properties, whose name are identical to the ARKit shapes in the CSV files from LLF, like &lt;em&gt;EyeBlinkLeft&lt;/em&gt; or &lt;em&gt;JawOpen&lt;/em&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We can therefore assume that these last 53 properties are correspond to the ARKit blendshapes from the app and should receive our key values during retargeting.&lt;br&gt;
&lt;br&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  How is Unreal computing animations?
&lt;/h2&gt;

&lt;p&gt;Before jumping into our favourite code editor, let's see how Unreal is reading and applying an animation onto the MetaHuman. I open the &lt;em&gt;Face_AnimBP&lt;/em&gt; blueprint from the project, which is the one handling live-retargeting facial animations from the LLF app onto the rig.&lt;br&gt;
&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--PsDZ0NEO--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ixtle4q4x2gov1310wfo.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--PsDZ0NEO--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ixtle4q4x2gov1310wfo.jpg" alt="blueprint" width="638" height="226"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;BINGO, there it is! Inside a &lt;em&gt;Live Link Face&lt;/em&gt; box, I find what seems to be a mapping block. It's doing all the connections. For each ARKit blendshape used by LLF, it is mapping the corresponding &lt;em&gt;CTRL&lt;/em&gt; expressions and &lt;em&gt;head&lt;/em&gt; correctives on the metahuman, by specifying an influence value between 0 (no influence) and 1 (full influence). You can see below how &lt;em&gt;BrowOuterUpLeft&lt;/em&gt; triggers the 3 expressions &lt;em&gt;browLateralL&lt;/em&gt;, &lt;em&gt;browRaiseInL&lt;/em&gt; and &lt;em&gt;browRaiseOuterL&lt;/em&gt; with different weights, while having no effect on any other expression.&lt;br&gt;
&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--EISjfI-A--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/aplzvo14rh8545oqtyup.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--EISjfI-A--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/aplzvo14rh8545oqtyup.jpg" alt="mapping" width="595" height="365"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;To confirm, I create random keys on the different properties in MotionBuilder and export the takes as FBX files to Unreal. It's working like a charm! &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Keys on the ARKit custom properties are read and retarget, but only if the animation goes through the mapping block first, so the animation blueprint is mandatory.&lt;/li&gt;
&lt;li&gt;However, if I put keys on the &lt;em&gt;CTRL&lt;/em&gt; shapes and &lt;em&gt;head&lt;/em&gt; correctives, I can apply my animation directy onto the MH, without the need of an animation blueprint.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;As you can see, we have two solutions taking shape here: The easy road where we simply transfer animation values from the CSV file to the ARKit blendshape properties, or the long road where we convert them into MH expression and corrective values. Trying both?&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--RwFPwxw---/c_limit%2Cf_auto%2Cfl_progressive%2Cq_66%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/enbxs0kyyq903b7yxxjz.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--RwFPwxw---/c_limit%2Cf_auto%2Cfl_progressive%2Cq_66%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/enbxs0kyyq903b7yxxjz.gif" alt="Challenge accepted" width="300" height="219"&gt;&lt;/a&gt;&lt;br&gt;
&lt;br&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Retargeting on ARKit blendshapes
&lt;/h2&gt;

&lt;p&gt;As said, this is the easy stroll in the park. Our CSV file and our root bone are sharing the same ARKit blendshape information. All we need to do is transfer the keys. Here are the main steps I am following:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Use Pandas to read the CSV file and extract all data. I create for this a BlendShape class, which receives the ARKit shape name, as well as animation keys, as pairs of timecode/values.&lt;/li&gt;
&lt;li&gt;For all BlendShape objects created, I search on the skeleton root for the corresponding property name and create keys for all timecode/value pairs.&lt;/li&gt;
&lt;li&gt;The scene framerate is very important here and should match the app's one, or keys won't be created at the correct frames.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Once done, adjust your timespan to frame the animation and have a look at the ARKit properties of the root. They should be animated.&lt;/p&gt;

&lt;p&gt;Export your animation as FBX to Unreal and test it in the Face_AnimBP blueprint. Don't forget it needs to go through that mapping block so Unreal can convert ARKit blendshape values to MH property values, which will be then turned into joint information. Cool beans!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--Fny5Uv60--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_66%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/to0w35lj7j8b1jz9vujo.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--Fny5Uv60--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_66%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/to0w35lj7j8b1jz9vujo.gif" alt="arkit shapes retarget" width="660" height="408"&gt;&lt;/a&gt;&lt;br&gt;
&lt;br&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Retargeting on MetaHuman properties
&lt;/h2&gt;

&lt;p&gt;Now let's get our hands dirty and explore that second solution! We want to be able to use a retarget file directly onto our MH character without going through all this mapping knick-knacks. But for that we need to find a way to recreate this mapping node in MotionBuilder. As a reminder, it is linking each ARKit shape to the corresponding MH &lt;em&gt;ctrl&lt;/em&gt; and &lt;em&gt;head&lt;/em&gt; properties with weighted values ranging from 0 to 1. So we first need to extract these values and weights from Unreal 🥵 &lt;/p&gt;

&lt;p&gt;After looking around for a while, I noticed that Unreal nodes can be exported as T3D ascii files, which contain all information on what the node is doing. I tried and I must admit that it wasn't love at first sight. Fortunately, it's only 13 lines long!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--bIzBGc7g--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_66%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/2h9c18n446v2mv7fzadp.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--bIzBGc7g--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_66%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/2h9c18n446v2mv7fzadp.gif" alt="T3D mapping file" width="660" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;But let's not judge a book by its cover. Again, if you take a closer look, everything we need is here. Time to see if my regex ninja training has paid off. After playing around with three patterns, I manage to extract from the T3D file:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The ARKit blendshapes names&lt;/li&gt;
&lt;li&gt;The MH properties names&lt;/li&gt;
&lt;li&gt;The mapping weights for each property
I add to my BlendShape class (characterising an ARKit shape), the target MH properties and the mapping weights. Done! I now have all the information I need for retargeting.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Back to MotionBuilder, I can run my magic loops again. This time we have to be a bit more careful though:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Read the T3D file and create BlendShape instances for all ARKit shapes.&lt;/li&gt;
&lt;li&gt;Use Pandas to read the CSV file and extract the animation keys for each ARKit shape. I fill up these timecode/value pairs into my BlendShape class instances.&lt;/li&gt;
&lt;li&gt;MotionBuilder is not very fast at finding root properties, maybe due to the high amount. To avoid searching for the same ones multiple times, I loop through the MH properties first and then through the timecode. For each frame, I check which ARKit blendshapes triggers my property and get the animation value weighted by my mapping information. For instance, if the ARKit value is 0.8 and the mapping value for this property is 0.5, then my animation key will be &lt;em&gt;0.8x0.5=0.4&lt;/em&gt;. If multiple ARKit shapes are triggering it, I take the max value corresponding to the highest influence.&lt;/li&gt;
&lt;li&gt;Again, be careful to retarget at the same framerate you used when recording takes.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I see keys on my root's properties! Let's export it as FBX to Unreal and assign it to my Face component. &lt;strong&gt;WUNDERBAR!&lt;/strong&gt; Unreal is reading the animation and transfering it to the facial joints without having to go through that mapping node anymore.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--xAS4UkTF--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_66%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/qcwplulr4rrkuoyulpar.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--xAS4UkTF--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_66%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/qcwplulr4rrkuoyulpar.gif" alt="Property retarget" width="660" height="400"&gt;&lt;/a&gt;&lt;br&gt;
&lt;br&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrapping up
&lt;/h2&gt;

&lt;p&gt;To finalise the script and make it more user-friendly, I added the following features:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A batching option, in case you point to a folder containing multiple CSV files&lt;/li&gt;
&lt;li&gt;The possibility to offset the starting timecode, to synchronise the animation file with other sources&lt;/li&gt;
&lt;li&gt;A simple PySyde UI for the user to enter the required paths files&lt;/li&gt;
&lt;li&gt;Some logs and exception handlings to make sure that our batch is performing properly&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We are done, good job everyone! 😉&lt;br&gt;
&lt;br&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;It only took patience and observation, as well as some ancient Regex voodoo to transfer the animation values recorded by the Live Link Face app as CSV files onto our brand new MetaHuman in Unreal. While not perfect, the result is good enough to move the animation to a cleaning stage. &lt;/p&gt;

&lt;p&gt;This is therefore a quick way to record facial animations from your actors, even if they cannot perform in real-time. The ability to process them at a later stage using an animation software like MotionBuilder and a fairly simple python script like this one makes of Live Link Face a cheap and reliable  facial motion-capture solution for MetaHumans characters.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--7-xl7Ns4--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_66%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/yrndowqn4uizqslv26ig.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--7-xl7Ns4--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_66%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/yrndowqn4uizqslv26ig.gif" alt="wink" width="200" height="221"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I hope this will help you write your own solution. Feel free to message me if you need any help or buy me a croissant and cappuccino next time you're in town 🥐☕ Cheers!&lt;/p&gt;

</description>
      <category>python</category>
      <category>motionbuilder</category>
      <category>animation</category>
      <category>unreal</category>
    </item>
    <item>
      <title>A python regex to validate roman numerals</title>
      <dc:creator>Alexandre Donciu-Julin</dc:creator>
      <pubDate>Fri, 09 Apr 2021 23:13:41 +0000</pubDate>
      <link>https://dev.to/alexdjulin/a-python-regex-to-validate-roman-numerals-2g99</link>
      <guid>https://dev.to/alexdjulin/a-python-regex-to-validate-roman-numerals-2g99</guid>
      <description>&lt;p&gt;&lt;em&gt;Note: This is my first post, I hope you'll like it :)&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  I'm not gonna lie to you... I LOVE REGEX!
&lt;/h3&gt;

&lt;p&gt;As a kid, I grew up playing adventure games full of puzzles and riddles. Looking for the solution was a personal quest, a treasure hunt. Finding it was so exciting, but not as much as jumping on another riddle!&lt;/p&gt;

&lt;p&gt;When I discovered regular expressions on my journey to become a (good) python programmer, I felt that same excitement. I was blown away by the countless possibilities these were offering. Deciphering one was like suddenly being able to read hieroglyph, writing one was like discovering I could speak a foreign language. Although I know they should be used with caution and in special cases only, I keep pushing myself to use them everywhere I can. &lt;/p&gt;

&lt;p&gt;That's why, when &lt;a href="https://www.codewars.com/kata/51b66044bce5799a7f000003/train/python" rel="noopener noreferrer"&gt;Codewars&lt;/a&gt; challenged me to write a function to convert roman numerals from/to Arabic numbers, I could not resist writing a regex to help me solve that problem.&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%2F4s41hungkq8yztw9nsj3.jpg" 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%2F4s41hungkq8yztw9nsj3.jpg" alt="Alt Text"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Enough chit-chat, let's get our hands dirty.
&lt;/h3&gt;

&lt;p&gt;My first task was to validate if the user input was a valid roman numeral. To sum up, roman numerals consists of the following symbols:&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%2Fcte88ws99u84t39vn7uy.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%2Fcte88ws99u84t39vn7uy.png" alt="image"&gt;&lt;/a&gt;Source: &lt;a href="https://en.wikipedia.org/wiki/Roman_numerals" rel="noopener noreferrer"&gt;Wikipedia&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It seems that the thousands unit [M] does not extend past [MMM], which means that the biggest roman number would be [MMMCMXCIX], or 3999. I'm not sure if numbers could go higher than that and why the limit, anyway for the sake of this problem I limited myself with numbers between 1 and 3999.&lt;/p&gt;

&lt;p&gt;Now the trick is that the symbols placement is very important. If you don't put them in the right order, the resulting number would be invalid and unreadable. As listed in the table up there, numerals should start with thousands [M] 1000, followed by hundreds [D/C] 500/100, then dozens [L/X] 50/10, and finally units [V/1] 5/1.&lt;/p&gt;

&lt;p&gt;BUT, that's not it! Numerals can only repeat 3 times like [CCC] 300 before switching to a combo of two numerals like [CD] 400. So you can still have a [C] 100 before an [M] 1000, like in [CM] 900 for instance.&lt;/p&gt;

&lt;p&gt;Bit confusing, isn't it? &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%2Fhyd4koybrd8kqlqrf8be.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%2Fhyd4koybrd8kqlqrf8be.png" alt="image"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Alright, let's recap our conditions:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Roman numbers are ranging from [I] 1 to [MMMCMXCIX] 3999&lt;/li&gt;
&lt;li&gt;Numerals should follow a precise order: [M] 1000 / [D] 500 / [C] 100 / [L] 50 / [X] 10 / [V] 5 / [I] 1&lt;/li&gt;
&lt;li&gt;A numeral cannot repeat more than 3 times, it then uses a pair&lt;/li&gt;
&lt;li&gt;The following pairs are allowed: [CM] 900 / [CD] 400 / [XC] 90 / [XL] 40 / [IX] 9 / [IV] 4&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Do you start to see our REGEX showing up? :)&lt;/p&gt;

&lt;h3&gt;
  
  
  Let's translate this into code.
&lt;/h3&gt;

&lt;p&gt;For this we are going to use a tag I find really helpful when writing regex is the verbose one (&lt;em&gt;re.VERBOSE&lt;/em&gt; or &lt;em&gt;re.X&lt;/em&gt;) It allows you to spread your pattern on multiple lines and be more readable. Let's try it!&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;re&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;is_roman_number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;num&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;

    &lt;span class="n"&gt;pattern&lt;/span&gt; &lt;span class="o"&gt;=&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;compile&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;   
                                ^M{0,3}
                                (CM|CD|D?C{0,3})?
                                (XC|XL|L?X{0,3})?
                                (IX|IV|V?I{0,3})?$
            &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;VERBOSE&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;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pattern&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;num&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;True&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;


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

&lt;/div&gt;

&lt;p&gt;Wow, that looks amazing already! Let's take a closer look at these 4 lines:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;^M{0,3}&lt;/strong&gt; = Between 0 and 3 [M] at the beginning [^] of the string&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;(CM|CD|D?C{0,3})?&lt;/strong&gt; = One pair [CM] or one pair [CD] or [D], followed by up to 3 [C]. Each element is optional [?], as well as the whole block [()?]&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;(XC|XL|L?X{0,3})?&lt;/strong&gt; = One pair [XC] or one pair [XL] or [L], followed by up to 3 [X]. Each element is optional [?], as well as the whole block [()?]&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;(IX|IV|V?I{0,3})?$&lt;/strong&gt; = One pair [IX] or one pair [IV] or [V], followed by up to 3 [I]. Each element is optional [?], as well as the whole block [()?], which should be at the end of the string [$]&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Let's test our code
&lt;/h3&gt;

&lt;p&gt;I'm using a simple fstring calling my function and comparing the string against our pattern to validate the numeral or not:&lt;/p&gt;

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


&lt;span class="n"&gt;num_valid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;MMDCCLXXIII&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;
&lt;span class="n"&gt;num_invalid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;CCCMMVIIVV&lt;/span&gt;&lt;span class="sh"&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;num_valid&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; is &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;not&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="nf"&gt;is_roman_number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;num_valid&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="sh"&gt;''&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;a roman number&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="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;num_invalid&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; is &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;not &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="nf"&gt;is_roman_number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;num_invalid&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="sh"&gt;''&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;a roman number&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Output:
# MMDCCLXXIII is a roman number
# CCCMMVIIVV is not a roman number
&lt;/span&gt;

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

&lt;/div&gt;

&lt;p&gt;That wasn't so bad after all! Now look at this and tell me it's not the most beautiful thing you've seen in your life:&lt;/p&gt;

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

&lt;span class="o"&gt;^&lt;/span&gt;&lt;span class="n"&gt;M&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="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;}(&lt;/span&gt;&lt;span class="n"&gt;CM&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;CD&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;D&lt;/span&gt;&lt;span class="err"&gt;?&lt;/span&gt;&lt;span class="n"&gt;C&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="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;&lt;span class="err"&gt;?&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;XC&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;XL&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;L&lt;/span&gt;&lt;span class="err"&gt;?&lt;/span&gt;&lt;span class="n"&gt;X&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="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;&lt;span class="err"&gt;?&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;IX&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;IV&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;V&lt;/span&gt;&lt;span class="err"&gt;?&lt;/span&gt;&lt;span class="n"&gt;I&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="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;&lt;span class="err"&gt;?$&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;


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

&lt;/div&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%2Fubznh8stkc79wcn5njdk.gif" 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%2Fubznh8stkc79wcn5njdk.gif" alt="Alt Text"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That's all folks! Let me know if you are interested by the second part of the challenge: converting roman numerals from/to Arabic numbers and I will share my solution.&lt;/p&gt;

&lt;p&gt;Stay safe out there and read you soon :)&lt;/p&gt;

</description>
      <category>python</category>
      <category>regex</category>
      <category>beginners</category>
    </item>
  </channel>
</rss>
