How we Automated our 100DaysOfCode Daily Tweet

By on 5 April 2017

In this article I show you a way to automatically tweet your #100DaysOfCode Challenge progress. This saves you some extra time to focus on the coding. Isn’t that all what matters?

This is day 007 of our 100 Days of Code challenge. You can follow along by forking our repo.

Getting ready

You need pytz, tweepy and requests. You can pip install -r requirements.txt if you cloned our repo (after cd-ing in 007). We recommend using virtualenv to isolate environments.

As explained in a previous article you need to get a Consumer Key/Secret and Access Token (Secret) from Twitter. I added those to my .bashrc which I load in via os.environ in config.py. There I also started a logging handler I use to log outgoing tweets and any exceptions that may occur.

The main script

See here and below what I learned:

  • As per PEP8 we import stdlib, followed by external modules and own project modules:

    import datetime
    import os
    import re
    import sys
    
    import requests
    import pytz
    
    from config import logging, api
    
  • My server (see deployment below) runs on MT tz and I wanted to talk EMEA times. Pytz (World Timezone Definitions for Python) to the rescue: it made working with timezones very easy:

    tz = pytz.timezone('Europe/Amsterdam')
    now = datetime.datetime.now(tz)
    start = datetime.datetime(2017, 3, 29, tzinfo=tz)  # = PyBites 100 days :)
    
  • I define some constants in all capital letters with underscores separating words (PEP8). I start to like datetime: calculating dates is easy:

    CURRENT_CHALLENGE_DAY = str((now - start).days).zfill(3)
    LOG = 'https://raw.githubusercontent.com/pybites/100DaysOfCode/master/LOG.md'
    LOG_ENTRY = re.compile(r'\[(?P.*?)\]\((?P<day>\d+)\)')
    REPO_URL = 'https://github.com/pybites/100DaysOfCode/tree/master/'
    TWEET_LEN = 140
    TWEET_LINK_LEN = 23
    </code></pre>
    </div>
    </li>
    <li>
    <p>Where would we be without requests? Here I get the LOG.md file from <a href="https://github.com/pybites/100DaysOfCode" target="_blank" rel="noopener">our repo</a>, just a single line of code:</p>
    <div class="hcb_wrap">
    <pre class="prism undefined-numbers lang-python" data-lang="Python"><code>def get_log():
        return requests.get(LOG).text.split('\n')
    </code></pre>
    </div>
    </li>
    <li>
    <p>I get the script title and day string from the line in LOG.md that matches the exact day string (today = ‘007’):</p>
    <div class="hcb_wrap">
    <pre class="prism undefined-numbers lang-python" data-lang="Python"><code>def get_day_progress(html):
        lines = [line.strip()
                for line in html
                if line.strip()]
    
        for line in lines:
            day_entry = line.strip('|').split('|')[0].strip()
            if day_entry == CURRENT_CHALLENGE_DAY:
                return LOG_ENTRY.search(line).groupdict()
    </code></pre>
    </div>
    </li>
    <li>
    <p>I create the tweet. I added some code to shorten the script title if the total tweet size is too long:</p>
    <div class="hcb_wrap">
    <pre class="prism undefined-numbers lang-python" data-lang="Python"><code>def create_tweet(m):
        ht1, ht2 = '#100DaysOfCode', '#Python'
        title = m['title']
        day = m['day']
        url = REPO_URL + day
        allowed_len = TWEET_LEN + len(url) - TWEET_LINK_LEN
    
        fmt = '{} - Day {}: {} {} {}'
        tweet = fmt.format(ht1, day, title, url, ht2)
        surplus = len(tweet) - allowed_len
    
        if surplus > 0:
            new_title = title[:-(surplus + 4)] + '...'
            tweet = tweet.replace(title, new_title)
        return tweet
    </code></pre>
    </div>
    </li>
    <li>
    <p>tweet_status() sends the tweet. We use the imported api object (from config.py) to send the tweet and we log an info if success, or error if any exception:</p>
    <div class="hcb_wrap">
    <pre class="prism undefined-numbers lang-python" data-lang="Python"><code>def tweet_status(tweet):
        try:
            api.update_status(tweet)
            logging.info('Posted to Twitter')
        except Exception as exc:
            logging.error('Error posting to Twitter: {}'.format(exc))
    </code></pre>
    </div>
    </li>
    <li>
    <p>We drive the script under main (= if script is run directly/standalone, not imported by another module). I set up some variables to allow for testing / dry runs:</p>
    <div class="hcb_wrap">
    <pre class="prism undefined-numbers lang-python" data-lang="Python"><code>if __name__ == '__main__':
        import socket
        local = 'MacBook' in socket.gethostname()
        test = local or 'dry' in sys.argv[1:]
    </code></pre>
    </div>
    </li>
    <li>
    <p>If test I use my local LOG file:</p>
    <div class="hcb_wrap">
    <pre class="prism undefined-numbers lang-python" data-lang="Python"><code>    if test:
            log = os.path.basename(LOG)
            with open(log) as f:
                html = f.readlines()
        else:
            html = get_log()
    </code></pre>
    </div>
    </li>
    <li>
    <p>If for some reason I don’t get a valid return from get_day_progress() I abort the script, logging the error:</p>
    <div class="hcb_wrap">
    <pre class="prism undefined-numbers lang-python" data-lang="Python"><code>    m = get_day_progress(html)
        if not m:
            logging.error('Error getting day progress from log')
            sys.exit(1)
    </code></pre>
    </div>
    </li>
    <li>
    <p>I create the tweet. If dry run, I just log it, else it tweets automatically:</p>
    <div class="hcb_wrap">
    <pre class="prism undefined-numbers lang-python" data-lang="Python"><code>    tweet = create_tweet(m)
        if test:
            logging.info('Test: tweet to send: {}'.format(tweet))
        else:
            tweet_status(tweet)
    </code></pre>
    </div>
    </li>
    </ul>
    <h2>Deployment</h2>
    <p>On my server I had to do some magic to get it all working: source .bashrc to load in the ENV vars, export PYTHONPATH, and specify the full path to python3. <a href="http://unix.stackexchange.com/a/27291" target="_blank" rel="noopener">As explained here</a>: “Cron knows nothing about your shell; it is started by the system, so it has a minimal environment.”</p>
    <div class="hcb_wrap">
    <pre class="prism undefined-numbers lang-python" data-lang="Python"><code>$ crontab -l
    ...
    34 14 * * * source $HOME/.bashrc && export PYTHONPATH=$HOME/bin/python3/lib/python3.5/site-packages && cd $HOME/code/100days/007 && $HOME/bin/python3/bin/python3.5 100day_autotweet.py
    </code></pre>
    </div>
    <h2>Result</h2>
    <p>What a coincidence: as I write this our <a href="https://twitter.com/pybites/status/849721815538712576" target="_blank" rel="noopener">today’s progress tweet just went out</a> 🙂</p>
    <p><img decoding="async" alt="my automated tweet" src="https://pybit.es/wp-content/uploads/2021/05/auto-tweet.png" class="aligncenter" title="How we Automated our 100DaysOfCode Daily Tweet 1"></p>
    <h2>Logging</h2>
    <p>The cool thing about the logging module is that you get the external packages’ logging for free. When I look at the log I see a lot more than my script’s logging:</p>
    <div class="hcb_wrap">
    <pre class="prism undefined-numbers lang-python" data-lang="Python"><code>$ vi 100day_autotweet.log
    ...
    ...
    14:34:02 tweepy.binder INFO     PARAMS: {'status': b'#100DaysOfCode - Day 007: script to automatically tweet 100DayOfCode progress tweet https://github.com/pybites/100DaysOfCode/tree/master/007 #Python'}
    ...
    many more log entries ...
    ...
    14:34:02 requests.packages.urllib3.connectionpool DEBUG    https://api.twitter.com:443 "POST /1.1/statuses/update.json?status=%23100DaysOfCode+-+Day+007%3A+script+to+automatically+tweet+100DayOfCode+progress+tweet+https%3A%2F%2Fgithub.com%2Fpybites%2F100DaysOfCode%2Ftree%2Fmaster%2F007+%23Python HTTP/1.1" 200 2693
    14:34:02 root         INFO     Posted to Twitter ==> my message
    </code></pre>
    </div>
    <p>Of course you can mute these by raising the log level (INFO or higher) in logging.basicConfig (<a href="https://github.com/pybites/100DaysOfCode/blob/master/007/config.py" target="_blank" rel="noopener">config.py</a>). See <a href="https://docs.python.org/3/library/logging.html" target="_blank" rel="noopener">the docs</a> for more info.</p>
    <hr>
    <p>I hope this taught you a bite of Python and it inspired you to automate your 100DaysOfCode and/or other tweets. Let us know how it goes … Happy coding!</p>
    <p>Keep Calm and Code in Python!</p>
    <p>— Bob</p>
    	</div><!-- .entry-content -->
    	
    <div class="call-to-action default-max-width">
    	<p><strong>Want a career as a Python Developer but not sure where to start?</strong></p>
    	<a href="mailto:info@pybit.es"><button>Ask Us a Question</button></a>
    	<a href="https://go.oncehub.com/pybiteschat"><button>Schedule a Strategy Call</button></a>
    </div>	<div class="related posts default-max-width">
    		<div id='jp-relatedposts' class='jp-relatedposts' >
    	<h3 class="jp-relatedposts-headline"><em>Related articles</em></h3>
    </div>	</div>
    	<footer class="entry-footer default-max-width">
    		
    
    <div class="share-article-links default-max-width">
    	<h2 class="footer-titles">Share This Article</h2>
    		<ul>
    			<li><a class="share-twitter" href="https://twitter.com/intent/tweet?text=How+we+Automated+our+100DaysOfCode+Daily+Tweet&url=https%3A%2F%2Fpybit.es%2Farticles%2F100days-autotweet%2F&via=share" target="_blank" rel="noopener noreferrer"><svg class="svg-icon" width="24" height="24" aria-hidden="true" role="img" focusable="false" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg"><path d="M22.23,5.924c-0.736,0.326-1.527,0.547-2.357,0.646c0.847-0.508,1.498-1.312,1.804-2.27 c-0.793,0.47-1.671,0.812-2.606,0.996C18.324,4.498,17.257,4,16.077,4c-2.266,0-4.103,1.837-4.103,4.103 c0,0.322,0.036,0.635,0.106,0.935C8.67,8.867,5.647,7.234,3.623,4.751C3.27,5.357,3.067,6.062,3.067,6.814 c0,1.424,0.724,2.679,1.825,3.415c-0.673-0.021-1.305-0.206-1.859-0.513c0,0.017,0,0.034,0,0.052c0,1.988,1.414,3.647,3.292,4.023 c-0.344,0.094-0.707,0.144-1.081,0.144c-0.264,0-0.521-0.026-0.772-0.074c0.522,1.63,2.038,2.816,3.833,2.85 c-1.404,1.1-3.174,1.756-5.096,1.756c-0.331,0-0.658-0.019-0.979-0.057c1.816,1.164,3.973,1.843,6.29,1.843 c7.547,0,11.675-6.252,11.675-11.675c0-0.178-0.004-0.355-0.012-0.531C20.985,7.47,21.68,6.747,22.23,5.924z"></path></svg></a></li>
    			<li><a class="share-linkedin" href="https://www.linkedin.com/shareArticle?mini=true&url=https%3A%2F%2Fpybit.es%2Farticles%2F100days-autotweet%2F&title=How+we+Automated+our+100DaysOfCode+Daily+Tweet" target="_blank" rel="noopener noreferrer"><svg class="svg-icon" width="24" height="24" aria-hidden="true" role="img" focusable="false" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg"><path d="M19.7,3H4.3C3.582,3,3,3.582,3,4.3v15.4C3,20.418,3.582,21,4.3,21h15.4c0.718,0,1.3-0.582,1.3-1.3V4.3 C21,3.582,20.418,3,19.7,3z M8.339,18.338H5.667v-8.59h2.672V18.338z M7.004,8.574c-0.857,0-1.549-0.694-1.549-1.548 c0-0.855,0.691-1.548,1.549-1.548c0.854,0,1.547,0.694,1.547,1.548C8.551,7.881,7.858,8.574,7.004,8.574z M18.339,18.338h-2.669 v-4.177c0-0.996-0.017-2.278-1.387-2.278c-1.389,0-1.601,1.086-1.601,2.206v4.249h-2.667v-8.59h2.559v1.174h0.037 c0.356-0.675,1.227-1.387,2.526-1.387c2.703,0,3.203,1.779,3.203,4.092V18.338z"></path></svg></a></li>
    			<li><a class="share-email" href="mailto:?subject=How we Automated our 100DaysOfCode Daily Tweet&body=https%3A%2F%2Fpybit.es%2Farticles%2F100days-autotweet%2F" target="_blank" rel="noopener noreferrer"><svg class="svg-icon" width="24" height="24" aria-hidden="true" role="img" focusable="false" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg"><path d="M20,4H4C2.895,4,2,4.895,2,6v12c0,1.105,0.895,2,2,2h16c1.105,0,2-0.895,2-2V6C22,4.895,21.105,4,20,4z M20,8.236l-8,4.882 L4,8.236V6h16V8.236z"></path></svg></a></li>
    		</ul>
    </div>		
    <div class="post-taxonomy default-max-width">
    	<h2 class="footer-titles">Read More</h2>
    	<ul>
    		<li class="post-taxonomy-highlights" ><a href="https://pybit.es/articles/">All Articles</a></li>
    		<li class="post-taxonomy-highlights"><a href="https://pybit.es/author/bobbelderbos/" rel="author">Author: Bob Belderbos</a></li>
    		<li class="post-taxonomy-highlights">
    			<a href="https://pybit.es/category/tools/" rel="category tag">Tools</a>		</li>
    
    		<li><a href="https://pybit.es/tag/100days/" rel="tag">100days</a></li><li><a href="https://pybit.es/tag/automation/" rel="tag">automation</a></li><li><a href="https://pybit.es/tag/logging/" rel="tag">logging</a></li><li><a href="https://pybit.es/tag/pytz/" rel="tag">pytz</a></li><li><a href="https://pybit.es/tag/tools/" rel="tag">tools</a></li><li><a href="https://pybit.es/tag/tweepy/" rel="tag">tweepy</a></li><li><a href="https://pybit.es/tag/twitter/" rel="tag">twitter</a></li>	</ul>
    </div>	</footer><!-- .entry-footer -->
    
    </article><!-- #post-6382 -->
    
    			</main><!-- #main -->
    		</div><!-- #primary -->
    	</div><!-- #content -->
    	
    	<aside class="widget-area">
    		<section id="nav_menu-11" class="widget widget_nav_menu"><h2 class="widget-title">PDM Program</h2><nav class="menu-pdm-program-container" aria-label="PDM Program"><ul id="menu-pdm-program" class="menu"><li id="menu-item-7064" class="menu-item menu-item-type-custom menu-item-object-custom menu-item-7064"><a href="https://pybites.circle.so/">Log In</a></li>
    <li id="menu-item-9467" class="menu-item menu-item-type-post_type menu-item-object-page menu-item-9467"><a href="https://pybit.es/catalogue/the-pdm-program/">About PDM</a></li>
    </ul></nav></section><section id="nav_menu-7" class="widget widget_nav_menu"><h2 class="widget-title">PyBites</h2><nav class="menu-pybites-container" aria-label="PyBites"><ul id="menu-pybites" class="menu"><li id="menu-item-7061" class="menu-item menu-item-type-post_type menu-item-object-page menu-item-7061"><a href="https://pybit.es/catalogue/">Catalogue</a></li>
    <li id="menu-item-6667" class="menu-item menu-item-type-custom menu-item-object-custom menu-item-6667"><a target="_blank" rel="noopener" href="https://www.pybitespodcast.com/">Podcast</a></li>
    <li id="menu-item-6956" class="menu-item menu-item-type-post_type menu-item-object-page current_page_parent menu-item-6956"><a href="https://pybit.es/articles/">Articles</a></li>
    <li id="menu-item-10035" class="menu-item menu-item-type-custom menu-item-object-custom menu-item-10035"><a href="https://codechalleng.es">Platform</a></li>
    <li id="menu-item-7466" class="menu-item menu-item-type-custom menu-item-object-custom menu-item-7466"><a href="https://pybit.es/community">Community</a></li>
    <li id="menu-item-11058" class="menu-item menu-item-type-post_type menu-item-object-page menu-item-11058"><a href="https://pybit.es/shop/">Shop</a></li>
    </ul></nav></section><section id="nav_menu-9" class="widget widget_nav_menu"><h2 class="widget-title">Resources</h2><nav class="menu-resources-container" aria-label="Resources"><ul id="menu-resources" class="menu"><li id="menu-item-6953" class="menu-item menu-item-type-post_type menu-item-object-page menu-item-6953"><a href="https://pybit.es/about-us/">About Us</a></li>
    <li id="menu-item-8379" class="menu-item menu-item-type-post_type menu-item-object-page menu-item-8379"><a href="https://pybit.es/contact-us/">Contact Us</a></li>
    <li id="menu-item-7062" class="menu-item menu-item-type-custom menu-item-object-custom menu-item-7062"><a href="https://pybit.es/wp-sitemap.xml">Sitemap</a></li>
    <li id="menu-item-6954" class="menu-item menu-item-type-post_type menu-item-object-page menu-item-6954"><a href="https://pybit.es/privacy-policy/">Privacy Policy</a></li>
    </ul></nav></section><section id="custom_html-10" class="widget_text widget widget_custom_html"><h2 class="widget-title">Mailing List</h2><div class="textwidget custom-html-widget"><div id="footer-mailing-list">
    	<p><em>Subscribe to our mailing list and receive the PyBites Effective Developer Package for free.</em></p>
    	<a data-sumome-listbuilder-id="d8f435f8-7688-4842-b976-ca3d80a31f9c" style="border-radius:10px"><button type="button">Subscribe	</button></a>
    </div></div></section>	</aside><!-- .widget-area -->
    
    	
    	<footer id="colophon" class="site-footer" role="contentinfo">
    
    		<div class="site-info">
    						<nav aria-label="Secondary menu" class="footer-navigation">
    				<ul class="footer-navigation-wrapper">
    					<li id="menu-item-6718" class="menu-item menu-item-type-custom menu-item-object-custom menu-item-6718"><a target="_blank" rel="noopener" href="https://twitter.com/pybites"><svg class="svg-icon" width="24" height="24" aria-hidden="true" role="img" focusable="false" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg"><path d="M22.23,5.924c-0.736,0.326-1.527,0.547-2.357,0.646c0.847-0.508,1.498-1.312,1.804-2.27 c-0.793,0.47-1.671,0.812-2.606,0.996C18.324,4.498,17.257,4,16.077,4c-2.266,0-4.103,1.837-4.103,4.103 c0,0.322,0.036,0.635,0.106,0.935C8.67,8.867,5.647,7.234,3.623,4.751C3.27,5.357,3.067,6.062,3.067,6.814 c0,1.424,0.724,2.679,1.825,3.415c-0.673-0.021-1.305-0.206-1.859-0.513c0,0.017,0,0.034,0,0.052c0,1.988,1.414,3.647,3.292,4.023 c-0.344,0.094-0.707,0.144-1.081,0.144c-0.264,0-0.521-0.026-0.772-0.074c0.522,1.63,2.038,2.816,3.833,2.85 c-1.404,1.1-3.174,1.756-5.096,1.756c-0.331,0-0.658-0.019-0.979-0.057c1.816,1.164,3.973,1.843,6.29,1.843 c7.547,0,11.675-6.252,11.675-11.675c0-0.178-0.004-0.355-0.012-0.531C20.985,7.47,21.68,6.747,22.23,5.924z"></path></svg><span class="screen-reader-text">Twitter</span></a></li>
    <li id="menu-item-6738" class="menu-item menu-item-type-custom menu-item-object-custom menu-item-6738"><a target="_blank" rel="noopener" href="https://www.linkedin.com/company/pybites/"><svg class="svg-icon" width="24" height="24" aria-hidden="true" role="img" focusable="false" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg"><path d="M19.7,3H4.3C3.582,3,3,3.582,3,4.3v15.4C3,20.418,3.582,21,4.3,21h15.4c0.718,0,1.3-0.582,1.3-1.3V4.3 C21,3.582,20.418,3,19.7,3z M8.339,18.338H5.667v-8.59h2.672V18.338z M7.004,8.574c-0.857,0-1.549-0.694-1.549-1.548 c0-0.855,0.691-1.548,1.549-1.548c0.854,0,1.547,0.694,1.547,1.548C8.551,7.881,7.858,8.574,7.004,8.574z M18.339,18.338h-2.669 v-4.177c0-0.996-0.017-2.278-1.387-2.278c-1.389,0-1.601,1.086-1.601,2.206v4.249h-2.667v-8.59h2.559v1.174h0.037 c0.356-0.675,1.227-1.387,2.526-1.387c2.703,0,3.203,1.779,3.203,4.092V18.338z"></path></svg><span class="screen-reader-text">LinkedIn</span></a></li>
    <li id="menu-item-6719" class="menu-item menu-item-type-custom menu-item-object-custom menu-item-6719"><a target="_blank" rel="noopener" href="https://facebook.com/pybites"><svg class="svg-icon" width="24" height="24" aria-hidden="true" role="img" focusable="false" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg"><path d="M12 2C6.5 2 2 6.5 2 12c0 5 3.7 9.1 8.4 9.9v-7H7.9V12h2.5V9.8c0-2.5 1.5-3.9 3.8-3.9 1.1 0 2.2.2 2.2.2v2.5h-1.3c-1.2 0-1.6.8-1.6 1.6V12h2.8l-.4 2.9h-2.3v7C18.3 21.1 22 17 22 12c0-5.5-4.5-10-10-10z"></path></svg><span class="screen-reader-text">Facebook</span></a></li>
    <li id="menu-item-6720" class="menu-item menu-item-type-custom menu-item-object-custom menu-item-6720"><a target="_blank" rel="noopener" href="https://www.youtube.com/channel/UCBn-uKDGsRBfcB0lQeOB_gA"><svg class="svg-icon" width="24" height="24" aria-hidden="true" role="img" focusable="false" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg"><path d="M21.8,8.001c0,0-0.195-1.378-0.795-1.985c-0.76-0.797-1.613-0.801-2.004-0.847c-2.799-0.202-6.997-0.202-6.997-0.202 h-0.009c0,0-4.198,0-6.997,0.202C4.608,5.216,3.756,5.22,2.995,6.016C2.395,6.623,2.2,8.001,2.2,8.001S2,9.62,2,11.238v1.517 c0,1.618,0.2,3.237,0.2,3.237s0.195,1.378,0.795,1.985c0.761,0.797,1.76,0.771,2.205,0.855c1.6,0.153,6.8,0.201,6.8,0.201 s4.203-0.006,7.001-0.209c0.391-0.047,1.243-0.051,2.004-0.847c0.6-0.607,0.795-1.985,0.795-1.985s0.2-1.618,0.2-3.237v-1.517 C22,9.62,21.8,8.001,21.8,8.001z M9.935,14.594l-0.001-5.62l5.404,2.82L9.935,14.594z"></path></svg><span class="screen-reader-text">YouTube</span></a></li>
    <li id="menu-item-6721" class="menu-item menu-item-type-custom menu-item-object-custom menu-item-6721"><a target="_blank" rel="noopener" href="https://github.com/PyBites-Open-Source"><svg class="svg-icon" width="24" height="24" aria-hidden="true" role="img" focusable="false" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg"><path d="M12,2C6.477,2,2,6.477,2,12c0,4.419,2.865,8.166,6.839,9.489c0.5,0.09,0.682-0.218,0.682-0.484 c0-0.236-0.009-0.866-0.014-1.699c-2.782,0.602-3.369-1.34-3.369-1.34c-0.455-1.157-1.11-1.465-1.11-1.465 c-0.909-0.62,0.069-0.608,0.069-0.608c1.004,0.071,1.532,1.03,1.532,1.03c0.891,1.529,2.341,1.089,2.91,0.833 c0.091-0.647,0.349-1.086,0.635-1.337c-2.22-0.251-4.555-1.111-4.555-4.943c0-1.091,0.39-1.984,1.03-2.682 C6.546,8.54,6.202,7.524,6.746,6.148c0,0,0.84-0.269,2.75,1.025C10.295,6.95,11.15,6.84,12,6.836 c0.85,0.004,1.705,0.114,2.504,0.336c1.909-1.294,2.748-1.025,2.748-1.025c0.546,1.376,0.202,2.394,0.1,2.646 c0.64,0.699,1.026,1.591,1.026,2.682c0,3.841-2.337,4.687-4.565,4.935c0.359,0.307,0.679,0.917,0.679,1.852 c0,1.335-0.012,2.415-0.012,2.741c0,0.269,0.18,0.579,0.688,0.481C19.138,20.161,22,16.416,22,12C22,6.477,17.523,2,12,2z"></path></svg><span class="screen-reader-text">Github</span></a></li>
    <li id="menu-item-8168" class="menu-item menu-item-type-custom menu-item-object-custom menu-item-8168"><a href="/feed"><svg class="svg-icon" width="24" height="24" aria-hidden="true" role="img" focusable="false" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg"><path d="M2,8.667V12c5.515,0,10,4.485,10,10h3.333C15.333,14.637,9.363,8.667,2,8.667z M2,2v3.333 c9.19,0,16.667,7.477,16.667,16.667H22C22,10.955,13.045,2,2,2z M4.5,17C3.118,17,2,18.12,2,19.5S3.118,22,4.5,22S7,20.88,7,19.5 S5.882,17,4.5,17z"></path></svg><span class="screen-reader-text">RSS</span></a></li>
    				</ul><!-- .footer-navigation-wrapper -->
    			</nav><!-- .footer-navigation -->
    						<div class="powered-by">
    				Powered by coffee, © 2024 PyBites.			</div><!-- .powered-by -->
    
    		</div><!-- .site-info -->
    	</footer><!-- #colophon -->
    </div><!-- #page -->
    
    <!-- Twitter universal website tag code -->
    <script>
    !function(e,t,n,s,u,a){e.twq||(s=e.twq=function(){s.exe?s.exe.apply(s,arguments):s.queue.push(arguments);
    },s.version='1.1',s.queue=[],u=t.createElement(n),u.async=!0,u.src='//static.ads-twitter.com/uwt.js',
    a=t.getElementsByTagName(n)[0],a.parentNode.insertBefore(u,a))}(window,document,'script');
    // Insert Twitter Pixel ID and Standard Event data below
    twq('init','o766g');
    twq('track','PageView');
    </script>
    <!-- End Twitter universal website tag code --><script async>(function(s,u,m,o,j,v){j=u.createElement(m);v=u.getElementsByTagName(m)[0];j.async=1;j.src=o;j.dataset.sumoSiteId='4262b3df2bb8820b75b4c7e09ef53c07d4d5376a3c0c6070d2fc19815019fc50';j.dataset.sumoPlatform='wordpress';v.parentNode.insertBefore(j,v)})(window,document,'script','//load.sumome.com/');</script>        <script type="application/javascript">
                const ajaxURL = "https://pybit.es/wp-admin/admin-ajax.php";
    
                function sumo_add_woocommerce_coupon(code) {
                    jQuery.post(ajaxURL, {
                        action: 'sumo_add_woocommerce_coupon',
                        code: code,
                    });
                }
    
                function sumo_remove_woocommerce_coupon(code) {
                    jQuery.post(ajaxURL, {
                        action: 'sumo_remove_woocommerce_coupon',
                        code: code,
                    });
                }
    
                function sumo_get_woocommerce_cart_subtotal(callback) {
                    jQuery.ajax({
                        method: 'POST',
                        url: ajaxURL,
                        dataType: 'html',
                        data: {
                            action: 'sumo_get_woocommerce_cart_subtotal',
                        },
                        success: function (subtotal) {
                            return callback(null, subtotal);
                        },
                        error: function (err) {
                            return callback(err, 0);
                        }
                    });
                }
            </script>
            <script>document.body.classList.remove("no-js");</script><button id="dark-mode-toggler" class="fixed-bottom" aria-pressed="false" onClick="toggleDarkMode()">Dark Mode: <span aria-hidden="true"></span></button>		<style>
    			#dark-mode-toggler > span {
    				margin-left: 5px;
    			}
    			#dark-mode-toggler > span::before {
    				content: 'Off';
    			}
    			#dark-mode-toggler[aria-pressed="true"] > span::before {
    				content: 'On';
    			}
    					</style>
    
    		<script>function toggleDarkMode() { // jshint ignore:line
    	var toggler = document.getElementById( 'dark-mode-toggler' );
    
    	if ( 'false' === toggler.getAttribute( 'aria-pressed' ) ) {
    		toggler.setAttribute( 'aria-pressed', 'true' );
    		document.documentElement.classList.add( 'is-dark-theme' );
    		document.body.classList.add( 'is-dark-theme' );
    		window.localStorage.setItem( 'twentytwentyoneDarkMode', 'yes' );
    	} else {
    		toggler.setAttribute( 'aria-pressed', 'false' );
    		document.documentElement.classList.remove( 'is-dark-theme' );
    		document.body.classList.remove( 'is-dark-theme' );
    		window.localStorage.setItem( 'twentytwentyoneDarkMode', 'no' );
    	}
    }
    
    function twentytwentyoneIsDarkMode() {
    	var isDarkMode = window.matchMedia( '(prefers-color-scheme: dark)' ).matches;
    
    	if ( 'yes' === window.localStorage.getItem( 'twentytwentyoneDarkMode' ) ) {
    		isDarkMode = true;
    	} else if ( 'no' === window.localStorage.getItem( 'twentytwentyoneDarkMode' ) ) {
    		isDarkMode = false;
    	}
    
    	return isDarkMode;
    }
    
    function darkModeInitialLoad() {
    	var toggler = document.getElementById( 'dark-mode-toggler' ),
    		isDarkMode = twentytwentyoneIsDarkMode();
    
    	if ( isDarkMode ) {
    		document.documentElement.classList.add( 'is-dark-theme' );
    		document.body.classList.add( 'is-dark-theme' );
    	} else {
    		document.documentElement.classList.remove( 'is-dark-theme' );
    		document.body.classList.remove( 'is-dark-theme' );
    	}
    
    	if ( toggler && isDarkMode ) {
    		toggler.setAttribute( 'aria-pressed', 'true' );
    	}
    }
    
    function darkModeRepositionTogglerOnScroll() {
    
    	var toggler = document.getElementById( 'dark-mode-toggler' ),
    		prevScroll = window.scrollY || document.documentElement.scrollTop,
    		currentScroll,
    
    		checkScroll = function() {
    			currentScroll = window.scrollY || document.documentElement.scrollTop;
    			if (
    				currentScroll + ( window.innerHeight * 1.5 ) > document.body.clientHeight ||
    				currentScroll < prevScroll
    			) {
    				toggler.classList.remove( 'hide' );
    			} else if ( currentScroll > prevScroll && 250 < currentScroll ) {
    				toggler.classList.add( 'hide' );
    			}
    			prevScroll = currentScroll;
    		};
    
    	if ( toggler ) {
    		window.addEventListener( 'scroll', checkScroll );
    	}
    }
    
    darkModeInitialLoad();
    darkModeRepositionTogglerOnScroll();
    </script>	<script>
    	if ( -1 !== navigator.userAgent.indexOf( 'MSIE' ) || -1 !== navigator.appVersion.indexOf( 'Trident/' ) ) {
    		document.body.classList.add( 'is-IE' );
    	}
    	</script>
    		<script>
    		(function () {
    			var c = document.body.className;
    			c = c.replace(/woocommerce-no-js/, 'woocommerce-js');
    			document.body.className = c;
    		})();
    	</script>
    	<link rel='stylesheet' id='wc-blocks-style-css' href='https://c0.wp.com/p/woocommerce/9.3.3/assets/client/blocks/wc-blocks.css' media='all' />
    <script src="https://pybit.es/wp-content/themes/twentytwentyone/assets/js/dark-mode-toggler.js?ver=1.0.0" id="twentytwentyone-dark-mode-support-toggle-js"></script>
    <script src="https://pybit.es/wp-content/themes/twentytwentyone/assets/js/editor-dark-mode-support.js?ver=1.0.0" id="twentytwentyone-editor-dark-mode-support-js"></script>
    <script src="https://pybit.es/wp-content/themes/twentytwentyone-child/assets/js/searchform-nav.js?ver=6.6.2" id="searchform-nav-js"></script>
    <script src="https://cdn.oncehub.com/mergedjs/so.js?ver=6.6.2" id="oncehub-js"></script>
    <script src="https://pybit.es/wp-content/themes/twentytwentyone/assets/js/responsive-embeds.js?ver=1.0" id="twenty-twenty-one-responsive-embeds-script-js"></script>
    <script src="https://c0.wp.com/p/woocommerce/9.3.3/assets/js/sourcebuster/sourcebuster.min.js" id="sourcebuster-js-js"></script>
    <script id="wc-order-attribution-js-extra">
    var wc_order_attribution = {"params":{"lifetime":1.0000000000000000818030539140313095458623138256371021270751953125e-5,"session":30,"base64":false,"ajaxurl":"https:\/\/pybit.es\/wp-admin\/admin-ajax.php","prefix":"wc_order_attribution_","allowTracking":true},"fields":{"source_type":"current.typ","referrer":"current_add.rf","utm_campaign":"current.cmp","utm_source":"current.src","utm_medium":"current.mdm","utm_content":"current.cnt","utm_id":"current.id","utm_term":"current.trm","utm_source_platform":"current.plt","utm_creative_format":"current.fmt","utm_marketing_tactic":"current.tct","session_entry":"current_add.ep","session_start_time":"current_add.fd","session_pages":"session.pgs","session_count":"udata.vst","user_agent":"udata.uag"}};
    </script>
    <script src="https://c0.wp.com/p/woocommerce/9.3.3/assets/js/frontend/order-attribution.min.js" id="wc-order-attribution-js"></script>
    <script src="https://pybit.es/wp-content/plugins/highlighting-code-block/assets/js/prism.js?ver=2.0.1" id="hcb-prism-js"></script>
    <script id="hcb-script-js-extra">
    var hcbVars = {"showCopyBtn":"","copyBtnLabel":"Copy code to clipboard"};
    </script>
    <script src="https://pybit.es/wp-content/plugins/highlighting-code-block/build/js/hcb_script.js?ver=2.0.1" id="hcb-script-js"></script>
    <script src="https://stats.wp.com/e-202445.js" id="jetpack-stats-js" data-wp-strategy="defer"></script>
    <script id="jetpack-stats-js-after">
    _stq = window._stq || [];
    _stq.push([ "view", JSON.parse("{\"v\":\"ext\",\"blog\":\"196712118\",\"post\":\"6382\",\"tz\":\"0\",\"srv\":\"pybit.es\",\"j\":\"1:13.9.1\"}") ]);
    _stq.push([ "clickTrackerInit", "196712118", "6382" ]);
    </script>
    
    </body>
    </html>