{"title":"Stanley Solutions Blog - Python","link":[{"@attributes":{"href":"https:\/\/blog.stanleysolutionsnw.com\/","rel":"alternate"}},{"@attributes":{"href":"https:\/\/blog.stanleysolutionsnw.com\/feeds\/python.atom.xml","rel":"self"}}],"id":"https:\/\/blog.stanleysolutionsnw.com\/","updated":"2026-04-15T21:44:00-07:00","subtitle":"engineering and creativity - all under one hat","entry":[{"title":"Triggering REAPER Recording from Python","link":{"@attributes":{"href":"https:\/\/blog.stanleysolutionsnw.com\/triggering-reaper-recording-from-python.html","rel":"alternate"}},"published":"2026-04-15T21:44:00-07:00","updated":"2026-04-15T21:44:00-07:00","author":{"name":"Joe Stanley"},"id":"tag:blog.stanleysolutionsnw.com,2026-04-15:\/triggering-reaper-recording-from-python.html","summary":"<p>Python, REAPER, and a Stream Deck? What do those things have in common? Is this one of those weird games? Not really. This is how I hooked up a Stream-Deck controller to control my REAPER audio software.<\/p>","content":"<p>Did you think you needed new ways of controlling your <a href=\"https:\/\/www.reaper.fm\/\">REAPER audio software<\/a>. I knew it! I knew it. Well, today is your lucky day.<\/p>\n<p>I did this a while back, and it was great... until I updated my Ubuntu installation, and started using a newer version of Python. Best I can tell, REAPER doesn't\nactually support Python 3.11 or newer. <em>Somebody want to prove me wrong? Leave a comment! I'd love to hear a better way to do this.<\/em> This left me flailing; looking\nfor a better solution so I could still have a big \"Record\" button on my Stream-Deck.<\/p>\n<p>This is all because REAPER has this cool system that allows you to interact with the software through a scripting interface, and even drive some actions directly\nthrough Python. Very cool for geeks like me!<\/p>\n<p>Now... this would've taken me quite a while, so I had a friend help me with some of the research. As this whole thing breaks down, you ultimately need to do the\nfollowing:<\/p>\n<ol>\n<li>Install <code>pyenv<\/code><\/li>\n<li>Install <strong>Python 3.10<\/strong> with shared-library support<\/li>\n<li>Configure REAPER to use that Python version<\/li>\n<li>Install <code>reapy-next<\/code><\/li>\n<li>Create a working <strong>toggle recording<\/strong> script<\/li>\n<\/ol>\n<hr>\n<h2>1. Install <code>pyenv<\/code><\/h2>\n<h3>Install dependencies<\/h3>\n<p>Here's a list of packages that will likely be needed to help get your <code>pyenv<\/code> system up and running.<\/p>\n<div class=\"highlight\"><pre><span><\/span><code>sudo<span class=\"w\"> <\/span>apt<span class=\"w\"> <\/span>update\nsudo<span class=\"w\"> <\/span>apt<span class=\"w\"> <\/span>install<span class=\"w\"> <\/span>-y<span class=\"w\"> <\/span>build-essential<span class=\"w\"> <\/span>libssl-dev<span class=\"w\"> <\/span>zlib1g-dev<span class=\"w\"> <\/span>\n<span class=\"w\">  <\/span>libbz2-dev<span class=\"w\"> <\/span>libreadline-dev<span class=\"w\"> <\/span>libsqlite3-dev<span class=\"w\"> <\/span>curl<span class=\"w\"> <\/span>\n<span class=\"w\">  <\/span>libncursesw5-dev<span class=\"w\"> <\/span>xz-utils<span class=\"w\"> <\/span>tk-dev<span class=\"w\"> <\/span>libxml2-dev<span class=\"w\"> <\/span>\n<span class=\"w\">  <\/span>libxmlsec1-dev<span class=\"w\"> <\/span>libffi-dev<span class=\"w\"> <\/span>liblzma-dev\n<\/code><\/pre><\/div>\n\n<h3>Install <code>pyenv<\/code><\/h3>\n<div class=\"highlight\"><pre><span><\/span><code>curl<span class=\"w\"> <\/span>https:\/\/pyenv.run<span class=\"w\"> <\/span><span class=\"p\">|<\/span><span class=\"w\"> <\/span>bash\n<\/code><\/pre><\/div>\n\n<h3>Add <code>pyenv<\/code> to your shell<\/h3>\n<p>Add these lines to <code>~\/.bashrc<\/code>:<\/p>\n<div class=\"highlight\"><pre><span><\/span><code><span class=\"nb\">export<\/span><span class=\"w\"> <\/span><span class=\"nv\">PATH<\/span><span class=\"o\">=<\/span><span class=\"s2\">&quot;<\/span><span class=\"nv\">$HOME<\/span><span class=\"s2\">\/.pyenv\/bin:<\/span><span class=\"nv\">$PATH<\/span><span class=\"s2\">&quot;<\/span>\n<span class=\"nb\">eval<\/span><span class=\"w\"> <\/span><span class=\"s2\">&quot;<\/span><span class=\"k\">$(<\/span>pyenv<span class=\"w\"> <\/span>init<span class=\"w\"> <\/span>-<span class=\"k\">)<\/span><span class=\"s2\">&quot;<\/span>\n<span class=\"nb\">eval<\/span><span class=\"w\"> <\/span><span class=\"s2\">&quot;<\/span><span class=\"k\">$(<\/span>pyenv<span class=\"w\"> <\/span>virtualenv-init<span class=\"w\"> <\/span>-<span class=\"k\">)<\/span><span class=\"s2\">&quot;<\/span>\n<\/code><\/pre><\/div>\n\n<p>Reload your shell:<\/p>\n<div class=\"highlight\"><pre><span><\/span><code><span class=\"nb\">source<\/span><span class=\"w\"> <\/span>~\/.bashrc\n<\/code><\/pre><\/div>\n\n<hr>\n<h2>2. Install Python 3.10 with shared-library support<\/h2>\n<p>REAPER <strong>requires<\/strong> the Python shared library (<code>libpython3.10.so<\/code>).<\/p>\n<p>Install Python 3.10:<\/p>\n<div class=\"highlight\"><pre><span><\/span><code>env<span class=\"w\"> <\/span><span class=\"nv\">PYTHON_CONFIGURE_OPTS<\/span><span class=\"o\">=<\/span><span class=\"s2\">&quot;--enable-shared&quot;<\/span><span class=\"w\"> <\/span>pyenv<span class=\"w\"> <\/span>install<span class=\"w\"> <\/span><span class=\"m\">3<\/span>.10.13\n<\/code><\/pre><\/div>\n\n<p>Set it as your working version:<\/p>\n<div class=\"highlight\"><pre><span><\/span><code>pyenv<span class=\"w\"> <\/span>shell<span class=\"w\"> <\/span><span class=\"m\">3<\/span>.10.13\n<\/code><\/pre><\/div>\n\n<p>Verify the <code>.so<\/code> exists:<\/p>\n<div class=\"highlight\"><pre><span><\/span><code>find<span class=\"w\"> <\/span>~\/.pyenv\/versions\/3.10.13<span class=\"w\"> <\/span>-name<span class=\"w\"> <\/span><span class=\"s2\">&quot;libpython3.10*.so&quot;<\/span>\n<\/code><\/pre><\/div>\n\n<p>Expected output:<\/p>\n<div class=\"highlight\"><pre><span><\/span><code>~\/.pyenv\/versions\/3.10.13\/lib\/libpython3.10.so\n<\/code><\/pre><\/div>\n\n<hr>\n<h2>3. Install <code>reapy-next<\/code><\/h2>\n<p><code>reapy-next<\/code> is the Python package that provides a whole slew of functionality\nfor interacting with REAPER through a Python lens. And hey, let's be honest... I'm basically blind to anything <em>other<\/em>\nthan Python.<\/p>\n<div class=\"highlight\"><pre><span><\/span><code>pip<span class=\"w\"> <\/span>install<span class=\"w\"> <\/span>reapy-next\n<\/code><\/pre><\/div>\n\n<hr>\n<h2>4. Configure REAPER to use <code>pyenv<\/code> Python 3.10<\/h2>\n<p>REAPER must be pointed at:<\/p>\n<ul>\n<li>The Python <strong>interpreter<\/strong><\/li>\n<li>The Python <strong>shared library (.so)<\/strong><\/li>\n<\/ul>\n<p>Open:<\/p>\n<blockquote>\n<p><strong>Options \u2192 Preferences \u2192 Plug-ins \u2192 ReaScript<\/strong><\/p>\n<\/blockquote>\n<p>Set:<\/p>\n<p><strong>Python interpreter:<\/strong><\/p>\n<div class=\"highlight\"><pre><span><\/span><code>\/home\/joestan\/.pyenv\/versions\/3.10.13\/lib\n<\/code><\/pre><\/div>\n\n<p><strong>Python library (.so):<\/strong><\/p>\n<div class=\"highlight\"><pre><span><\/span><code>libpython3.10.so\n<\/code><\/pre><\/div>\n\n<p>Restart REAPER.<\/p>\n<hr>\n<h2>5. Ensure REAPER actually uses Python 3.10<\/h2>\n<p>With REAPER open, run the following terminal command:<\/p>\n<div class=\"highlight\"><pre><span><\/span><code>~\/.pyenv\/versions\/3.10.13\/bin\/python3<span class=\"w\"> <\/span>-c<span class=\"w\"> <\/span><span class=\"s2\">&quot;import reapy; reapy.configure_reaper()&quot;<\/span>\n<\/code><\/pre><\/div>\n\n<p>That command should immediately return without any error. If it hangs, and a message appears in REAPER regarding some\nPython 3.12 path that cannot be found, it's time to get to <code>grep<\/code>in'.<\/p>\n<h3>5.1 Fix REAPER config files<\/h3>\n<p>If you saw Python 3.12 above, it's time to fix your <code>reaper.ini<\/code>. I had to do some searching to actually find that.<\/p>\n<p>Run the following command to find any locations that are still referencing Python 3.12<\/p>\n<div class=\"highlight\"><pre><span><\/span><code>grep<span class=\"w\"> <\/span>-Rni<span class=\"w\"> <\/span><span class=\"s2\">&quot;python3.12&quot;<\/span><span class=\"w\"> <\/span>~\/.config\/REAPER\n<\/code><\/pre><\/div>\n\n<p>Then go pop those files open and change those full Python paths to the <code>pyenv<\/code> equivalents shown above.<\/p>\n<p>Restart REAPER again.<\/p>\n<hr>\n<h2>6. Install the <code>ReaCmd<\/code> Package<\/h2>\n<p>That's right. You guessed it.<\/p>\n<blockquote>\n<p>I wrote another Python package.<\/p>\n<\/blockquote>\n<p>But this one's small! You can snag it from <a href=\"https:\/\/github.com\/engineerjoe440\/ReaCmd\">GitHub<\/a>. To make it easy, just\ninstall that package in your <code>pyenv<\/code> environment:<\/p>\n<div class=\"highlight\"><pre><span><\/span><code>~\/.pyenv\/versions\/3.10.13\/bin\/python3<span class=\"w\"> <\/span>-m<span class=\"w\"> <\/span>pip<span class=\"w\"> <\/span>install<span class=\"w\"> <\/span>git+https:\/\/github.com\/engineerjoe440\/ReaCmd.git\n<\/code><\/pre><\/div>\n\n<p>Now you should have a script accessible to actually operate the record-toggle operation.<\/p>\n<div class=\"highlight\"><pre><span><\/span><code>~\/.pyenv\/versions\/3.10.13\/bin\/rea-record\n<\/code><\/pre><\/div>\n\n<p><em>Voila<\/em><\/p>\n<hr>\n<h1>Done.<\/h1>\n<p>You now have:<\/p>\n<ul>\n<li>A working pyenv Python 3.10 installation<\/li>\n<li>REAPER correctly embedding Python 3.10<\/li>\n<li>A working <code>reapy-next<\/code> client script that toggles recording<\/li>\n<\/ul>\n<p>I hope you find this helpful. I'm sure I'll be coming back to that to fix my system again in the future!<\/p>","category":[{"@attributes":{"term":"Python"}},{"@attributes":{"term":"audio"}},{"@attributes":{"term":"daw"}},{"@attributes":{"term":"python"}},{"@attributes":{"term":"reaper"}},{"@attributes":{"term":"scripting"}},{"@attributes":{"term":"stream-deck"}},{"@attributes":{"term":"ubuntu"}},{"@attributes":{"term":"linux"}}]},{"title":"More Python Nonsense","link":{"@attributes":{"href":"https:\/\/blog.stanleysolutionsnw.com\/more-python-nonsense.html","rel":"alternate"}},"published":"2026-04-14T12:07:00-07:00","updated":"2026-04-14T12:07:00-07:00","author":{"name":"Joe Stanley"},"id":"tag:blog.stanleysolutionsnw.com,2026-04-14:\/more-python-nonsense.html","summary":"<p>You asked... so here it is! A whole bunch of random Python goodies. Things to help you get started, to learn about the language, to go deeper... and more!<\/p>","content":"<p>Python is my favorite programming language. There's no doubt about that! I've built a lot of random things with it;\neverything from helper scripts, to web-scrapers, to websites, and beyond.<\/p>\n<p>I've had some questions, recently about where I get my Python resources, so I thought I'd share some of that goodness...<\/p>\n<h3>Podcasts<\/h3>\n<blockquote>\n<p>Everybody's got a podcast.<\/p>\n<\/blockquote>\n<p>That's true, but sometimes, that's because there are so many interesting things to discuss. That's especially true in the\nPython\/coding world. There are other Python podcasts, like the <a href=\"https:\/\/realpython.com\/podcasts\/rpp\/\"><em>Real Python Podcast<\/em><\/a>,\nbut the ones below are some of my favorites. I've listed them before in <a href=\"\/tech-podcasts-galore.html\">another blog post<\/a>,\nbut it's always nice to remind myself about them.<\/p>\n<h4><em>\"Python Bytes\"<\/em><\/h4>\n<p><img src=\"https:\/\/pythonbytes.fm\/static\/img\/logo.png?cache_id=391cb49247369a67c4be78b27f2b3cd5\"\n    width=\"150\" alt=\"Python Bytes\" align=\"left\"><\/p>\n<p>By far, my favorite of the programming-pods, <a href=\"https:\/\/pythonbytes.fm\/\"><em>Python Bytes<\/em><\/a> covers the latest news\nin the Python programming language and the areas of the tech world that Python supports. Everything from web\nservers to data-science, from embedded Python to the data-center!<\/p>\n<p><em>Python Bytes<\/em> is a little on the shorter side; at least when compared with the other podcasts I've listed so\nfar. Michael Kennedy and Brian Okken cover all the latest-and-greatest Python modules and techniques with at\nleast one guest host every week.<\/p>\n<h4><em>\"Talk Python to Me\"<\/em><\/h4>\n<p>Another podcast from Michael Kennedy (one of the hosts of the aforementioned <em>Python Bytes<\/em>) is\n<a href=\"https:\/\/talkpython.fm\"><em>Talk Python to Me<\/em><\/a>. It's a little longer than its sister podcast, and goes into\ngreater detail in the technology or topic of interest. Michael brings on a variety of fantastic guests, all\nof which help discuss the latest news.<\/p>\n<h4><em>\"Test and Code\"<\/em><\/h4>\n<p>Brian Okken's other podcast, much like <em>Talk Python to Me<\/em> is a companion podcast to <em>Python Bytes<\/em>. Brian,\nthe author of <em>\"Python Testing with pytest: Simple, Rapid, Effective, and Scalable\"<\/em> covers the intricacies\nof testing code effectively. <a href=\"https:\/\/testandcode.com\/\"><em>Test and Code<\/em><\/a> is a great podcast, and is a little\non the shorter side, so it makes for quick listening!<\/p>\n<hr>\n<h3>Websites<\/h3>\n<p>There are a <em>lot<\/em> of great resources when it comes to Python on the web, but here's just a few...<\/p>\n<ul>\n<li><a href=\"https:\/\/www.geeksforgeeks.org\/python\/python-programming-language-tutorial\/\"><em>Geeks for Geeks<\/em><\/a><\/li>\n<li><a href=\"https:\/\/pythongeeks.org\/\"><em>Python Geeks<\/em><\/a><\/li>\n<li><a href=\"https:\/\/www.w3schools.com\/python\/python_intro.asp\"><em>W3 Schools<\/em><\/a><\/li>\n<li><a href=\"https:\/\/codecombat.com\/\"><em>Code Combat<\/em><\/a><\/li>\n<li><a href=\"https:\/\/www.boot.dev\/courses\/learn-code-python\"><em>boot.dev<\/em><\/a><\/li>\n<\/ul>\n<p>There's also plenty of cool projects. I've got to plug my own, but there's some other greats in here, too!<\/p>\n<ul>\n<li><a href=\"https:\/\/github.com\/engineerjoe440\/ElectricPy\"><em>ElectricPy<\/em><\/a><\/li>\n<li><a href=\"https:\/\/github.com\/engineerjoe440\/phasors\"><em>Phasors<\/em><\/a><\/li>\n<li><a href=\"https:\/\/github.com\/engineerjoe440\/selprotopy\"><em>SELProtoPy<\/em><\/a><\/li>\n<li><a href=\"https:\/\/github.com\/engineerjoe440\/pycev\"><em>pyCEV<\/em><\/a><\/li>\n<li><a href=\"https:\/\/fastapi.tiangolo.com\/\"><em>FastAPI<\/em><\/a><\/li>\n<li><a href=\"https:\/\/numpy.org\/\"><em>NumPy<\/em><\/a><\/li>\n<li><a href=\"https:\/\/pydantic.dev\/docs\/validation\/latest\/get-started\/\"><em>Pydantic<\/em><\/a><\/li>\n<li><a href=\"https:\/\/www.djangoproject.com\/\"><em>django<\/em><\/a><\/li>\n<\/ul>\n<hr>\n<h3>Email Lists<\/h3>\n<p>Don't like searching for your information? Want it brought right to you in some form of e-news\/letter format. Have\nyou considered email?<\/p>\n<h4><em>\"Python Weekly\"<\/em><\/h4>\n<p>This is a great newsletter, one of my favorites. They often have good \"how-to\" articles, links to online discussions,\nand they often include some great projects just to show them off! <a href=\"https:\/\/www.pythonweekly.com\/\"><em>Python Weekly<\/em><\/a> is\none of the few email subscriptions I actually read.<\/p>\n<h4><em>\"PyCoders Weekly\"<\/em><\/h4>\n<p>Speaking of newsletters I actually read, <a href=\"https:\/\/pycoders.com\/\"><em>PyCoders Weekly<\/em><\/a> is another one of my favorites, and\nI look forward to its weekly delivery. I know I'll see neat projects and see articles that will give me pause to think, or share with a friend or colleague. This is another one of the great news resources for Python.<\/p>\n<h4>The Official <em>\"PSF Newsletter\"<\/em><\/h4>\n<p>Of course, I'd be remiss if I didn't at least mention the official <a href=\"https:\/\/www.python.org\/psf\/newsletter\/\"><em>PSF Newsletter<\/em><\/a>.\nPut together by the Python Software Foundation, or PSF, this newsletter comes with interesting information about what's\nhappening in the Python ecosystem. New PEPs, new projects, news, news, news!<\/p>\n<hr>\n<h3>Github and Other Resources<\/h3>\n<p>What would this list be without an \"awesome list\"? They've become popular on Github, so I better list the one for Pyton:\n<a href=\"https:\/\/github.com\/vinta\/awesome-python\">https:\/\/github.com\/vinta\/awesome-python<\/a> There's plenty of good stuff in there, so go take a look!<\/p>\n<hr>\n<p>I could go on for days, here, but I've got other things to go do. So that'll do for now.<\/p>","category":[{"@attributes":{"term":"Python"}},{"@attributes":{"term":"education"}},{"@attributes":{"term":"electrical-engineering"}},{"@attributes":{"term":"learning"}},{"@attributes":{"term":"online"}},{"@attributes":{"term":"programming"}},{"@attributes":{"term":"python"}},{"@attributes":{"term":"resources"}},{"@attributes":{"term":"teaching"}}]},{"title":"Smello World? For HTTP Requests","link":{"@attributes":{"href":"https:\/\/blog.stanleysolutionsnw.com\/smello-world-with-http.html","rel":"alternate"}},"published":"2026-03-31T13:47:00-07:00","updated":"2026-03-31T13:47:00-07:00","author":{"name":"Joe Stanley"},"id":"tag:blog.stanleysolutionsnw.com,2026-03-31:\/smello-world-with-http.html","summary":"<p>A short article, but hopefully getting a jump on getting back in the habbit of documenting what I'm up to. Check out this cool tool!<\/p>","content":"<p>OK... It's been a while.<\/p>\n<p>Hi, my name is Joe.<\/p>\n<p>I'm here today to tell you about a neat little tool that I came across in one of the email subscriptions I have\nfor Python. Yes... I'm one of <em>those<\/em> guys. I like to see all the cool new stuff happening in the Python sphere.<\/p>\n<p>All I really have to say is check this out...<\/p>\n<p><img alt=\"Hello, Smello!\" src=\".\/images\/smello.png\" width=\"100%\"><\/p>\n<p>Let me introduce you to <a href=\"https:\/\/github.com\/smelloscope\/smello\">Smello<\/a>, an MIT-licensed tool to help you capture\nHTTP requests from your Python app and log them for analysis. Go read up on their\n<a href=\"https:\/\/roman.pt\/posts\/smello\/\">blog post<\/a> for some more information, but let me say... this thing is neat! I'll'\ndefinitely need to try it out sometime soon.<\/p>","category":[{"@attributes":{"term":"Python"}},{"@attributes":{"term":"python"}},{"@attributes":{"term":"http"}},{"@attributes":{"term":"web"}},{"@attributes":{"term":"requests"}},{"@attributes":{"term":"httpx"}},{"@attributes":{"term":"rest"}},{"@attributes":{"term":"api"}}]},{"title":"Making Configuration for a Python Application Simple!","link":{"@attributes":{"href":"https:\/\/blog.stanleysolutionsnw.com\/making-configuration-for-a-python-application-simple.html","rel":"alternate"}},"published":"2023-01-17T14:23:00-08:00","updated":"2023-01-17T14:23:00-08:00","author":{"name":"Joe Stanley"},"id":"tag:blog.stanleysolutionsnw.com,2023-01-17:\/making-configuration-for-a-python-application-simple.html","summary":"<p>If you're a nerd like me, you can probably think of your favorite self-hosted application right now. Better yet, you can probably think of all the reasons you love it. You know, one of the staples of a great self-hosted application is its ability to make the configuration <em>your own<\/em> and do so easily! I've been working on a number of little applications lately and they all need configuration, so I've started setting this up with the help of Python and TOML!<\/p>","content":"<p>If you know me, you'll know that I often have <em>way too many projects<\/em> all in process <em>at the same dang time<\/em>.<\/p>\n<blockquote>\n<p>So proud. So proud...<\/p>\n<\/blockquote>\n<p>Well, as a result of this, lately, I've been able to capitalize on some common work. Primarily surrounding the configuration management for these apps.\nConfiguration is a bit of a tricky subject, sometimes. Because often-times, it depends greatly on how the application will be hosted, how the configuration\nshould be set-up. But also, different devops folks will like different mechanisms to apply their config. After all, some folks like using nothing more than\nenvironment variables for EVERYTHING. This makes configuring an app with tools like <a href=\"https:\/\/docs.docker.com\/get-started\/08_using_compose\/\"><code>docker-compose<\/code><\/a>\na cinch. However, there are others who would rather set up their configuration with the file, itself. Thus, marrying the options can be a bit challenging at times.<\/p>\n<p>I've recently come into the awareness of the Python package <a href=\"https:\/\/pypi.org\/project\/toml-config\/\"><code>toml-config<\/code><\/a>. This simple little package wraps other\nPython libraries to support using TOML as the basis of configuration files.<\/p>\n<h2>What is TOML, anyway?<\/h2>\n<p>Well, if you'd like to go read for yourself, you can visit <a href=\"https:\/\/toml.io\/en\/\">the TOML website<\/a>. But here's a simple example:<\/p>\n<div class=\"highlight\"><pre><span><\/span><code><span class=\"c1\"># This is a TOML document<\/span>\n\n<span class=\"n\">title<\/span><span class=\"w\"> <\/span><span class=\"o\">=<\/span><span class=\"w\"> <\/span><span class=\"s2\">&quot;TOML Example&quot;<\/span>\n\n<span class=\"k\">[owner]<\/span>\n<span class=\"n\">name<\/span><span class=\"w\"> <\/span><span class=\"o\">=<\/span><span class=\"w\"> <\/span><span class=\"s2\">&quot;Tom Preston-Werner&quot;<\/span>\n<span class=\"n\">dob<\/span><span class=\"w\"> <\/span><span class=\"o\">=<\/span><span class=\"w\"> <\/span><span class=\"ld\">1979-05-27T07:32:00-08:00<\/span>\n\n<span class=\"k\">[database]<\/span>\n<span class=\"n\">enabled<\/span><span class=\"w\"> <\/span><span class=\"o\">=<\/span><span class=\"w\"> <\/span><span class=\"kc\">true<\/span>\n<span class=\"n\">ports<\/span><span class=\"w\"> <\/span><span class=\"o\">=<\/span><span class=\"w\"> <\/span><span class=\"p\">[<\/span><span class=\"w\"> <\/span><span class=\"mi\">8000<\/span><span class=\"p\">,<\/span><span class=\"w\"> <\/span><span class=\"mi\">8001<\/span><span class=\"p\">,<\/span><span class=\"w\"> <\/span><span class=\"mi\">8002<\/span><span class=\"w\"> <\/span><span class=\"p\">]<\/span>\n<span class=\"n\">data<\/span><span class=\"w\"> <\/span><span class=\"o\">=<\/span><span class=\"w\"> <\/span><span class=\"p\">[<\/span><span class=\"w\"> <\/span><span class=\"p\">[<\/span><span class=\"s2\">&quot;delta&quot;<\/span><span class=\"p\">,<\/span><span class=\"w\"> <\/span><span class=\"s2\">&quot;phi&quot;<\/span><span class=\"p\">],<\/span><span class=\"w\"> <\/span><span class=\"p\">[<\/span><span class=\"mf\">3.14<\/span><span class=\"p\">]<\/span><span class=\"w\"> <\/span><span class=\"p\">]<\/span>\n<span class=\"n\">temp_targets<\/span><span class=\"w\"> <\/span><span class=\"o\">=<\/span><span class=\"w\"> <\/span><span class=\"p\">{<\/span><span class=\"w\"> <\/span><span class=\"n\">cpu<\/span><span class=\"w\"> <\/span><span class=\"p\">=<\/span><span class=\"w\"> <\/span><span class=\"mf\">79.5<\/span><span class=\"p\">,<\/span><span class=\"w\"> <\/span><span class=\"n\">case<\/span><span class=\"w\"> <\/span><span class=\"p\">=<\/span><span class=\"w\"> <\/span><span class=\"mf\">72.0<\/span><span class=\"w\"> <\/span><span class=\"p\">}<\/span>\n\n<span class=\"k\">[servers]<\/span>\n\n<span class=\"k\">[servers.alpha]<\/span>\n<span class=\"n\">ip<\/span><span class=\"w\"> <\/span><span class=\"o\">=<\/span><span class=\"w\"> <\/span><span class=\"s2\">&quot;10.0.0.1&quot;<\/span>\n<span class=\"n\">role<\/span><span class=\"w\"> <\/span><span class=\"o\">=<\/span><span class=\"w\"> <\/span><span class=\"s2\">&quot;frontend&quot;<\/span>\n\n<span class=\"k\">[servers.beta]<\/span>\n<span class=\"n\">ip<\/span><span class=\"w\"> <\/span><span class=\"o\">=<\/span><span class=\"w\"> <\/span><span class=\"s2\">&quot;10.0.0.2&quot;<\/span>\n<span class=\"n\">role<\/span><span class=\"w\"> <\/span><span class=\"o\">=<\/span><span class=\"w\"> <\/span><span class=\"s2\">&quot;backend&quot;<\/span>\n<\/code><\/pre><\/div>\n\n<p>TOML is the basis for the modern Python packaging standard providing <code>pyproject.toml<\/code> files in place of the executable <code>setup.py<\/code>. That's another\nconversation, for a different day, perhaps I'll dive into that sometime soon.<\/p>\n<p>Anyway, TOML is a nice, concise way of describing settings and configuration options in an easily readable format. I'm a big fan of JSON as a general\nrule, but TOML makes configuration pretty easy to get started with.<\/p>\n<h2>How do I connect TOML and Environment Variables Easily?<\/h2>\n<p>Well, with that slick little tool, <code>toml-config<\/code>, I've been able to create a really nice little framework.<\/p>\n<div class=\"highlight\"><pre><span><\/span><code><span class=\"kn\">from<\/span> <span class=\"nn\">typing<\/span> <span class=\"kn\">import<\/span> <span class=\"n\">List<\/span>\n<span class=\"kn\">import<\/span> <span class=\"nn\">os<\/span>\n<span class=\"kn\">import<\/span> <span class=\"nn\">pathlib<\/span>\n<span class=\"kn\">from<\/span> <span class=\"nn\">toml_config.core<\/span> <span class=\"kn\">import<\/span> <span class=\"n\">Config<\/span>\n\n\n<span class=\"c1\"># Inject helper method to simplify modifying values on the fly.<\/span>\n<span class=\"k\">def<\/span> <span class=\"nf\">update<\/span><span class=\"p\">(<\/span><span class=\"bp\">self<\/span><span class=\"p\">:<\/span> <span class=\"n\">Config<\/span><span class=\"p\">,<\/span> <span class=\"n\">key_name<\/span><span class=\"p\">:<\/span> <span class=\"nb\">str<\/span><span class=\"p\">,<\/span> <span class=\"n\">value<\/span><span class=\"p\">:<\/span> <span class=\"nb\">str<\/span><span class=\"p\">):<\/span>\n<span class=\"w\">    <\/span><span class=\"sd\">&quot;&quot;&quot;Update the Specified Key Name - Section Independent.&quot;&quot;&quot;<\/span>\n    <span class=\"k\">for<\/span> <span class=\"n\">section<\/span><span class=\"p\">,<\/span> <span class=\"n\">data<\/span> <span class=\"ow\">in<\/span> <span class=\"bp\">self<\/span><span class=\"o\">.<\/span><span class=\"n\">config<\/span><span class=\"o\">.<\/span><span class=\"n\">items<\/span><span class=\"p\">():<\/span>\n        <span class=\"k\">if<\/span> <span class=\"n\">key_name<\/span> <span class=\"ow\">in<\/span> <span class=\"nb\">list<\/span><span class=\"p\">(<\/span><span class=\"n\">data<\/span><span class=\"o\">.<\/span><span class=\"n\">keys<\/span><span class=\"p\">()):<\/span>\n            <span class=\"bp\">self<\/span><span class=\"o\">.<\/span><span class=\"n\">get_section<\/span><span class=\"p\">(<\/span><span class=\"n\">section<\/span><span class=\"p\">)<\/span>\n            <span class=\"bp\">self<\/span><span class=\"o\">.<\/span><span class=\"n\">set<\/span><span class=\"p\">(<\/span><span class=\"o\">**<\/span><span class=\"p\">{<\/span><span class=\"n\">key_name<\/span><span class=\"p\">:<\/span> <span class=\"n\">value<\/span><span class=\"p\">})<\/span>\n<span class=\"n\">Config<\/span><span class=\"o\">.<\/span><span class=\"n\">update<\/span> <span class=\"o\">=<\/span> <span class=\"n\">update<\/span>\n\n\n\n<span class=\"k\">class<\/span> <span class=\"nc\">BaseConfig<\/span><span class=\"p\">(<\/span><span class=\"nb\">object<\/span><span class=\"p\">):<\/span>\n<span class=\"w\">    <\/span><span class=\"sd\">&quot;&quot;&quot;Base Configuration Object: Used for Inheritance for Additional Config.&quot;&quot;&quot;<\/span>\n    <span class=\"n\">_config<\/span><span class=\"p\">:<\/span> <span class=\"n\">Config<\/span>\n\n    <span class=\"nd\">@property<\/span>\n    <span class=\"k\">def<\/span> <span class=\"nf\">config<\/span><span class=\"p\">(<\/span><span class=\"bp\">self<\/span><span class=\"p\">):<\/span>\n<span class=\"w\">        <\/span><span class=\"sd\">&quot;&quot;&quot;Return the Full Configuration.&quot;&quot;&quot;<\/span>\n        <span class=\"k\">return<\/span> <span class=\"bp\">self<\/span><span class=\"o\">.<\/span><span class=\"n\">_config<\/span><span class=\"o\">.<\/span><span class=\"n\">config<\/span>\n\n    <span class=\"k\">def<\/span> <span class=\"fm\">__setattr__<\/span><span class=\"p\">(<\/span><span class=\"bp\">self<\/span><span class=\"p\">,<\/span> <span class=\"n\">name<\/span><span class=\"p\">:<\/span> <span class=\"nb\">str<\/span><span class=\"p\">,<\/span> <span class=\"n\">value<\/span><span class=\"p\">:<\/span> <span class=\"n\">Any<\/span><span class=\"p\">)<\/span> <span class=\"o\">-&gt;<\/span> <span class=\"kc\">None<\/span><span class=\"p\">:<\/span>\n<span class=\"w\">        <\/span><span class=\"sd\">&quot;&quot;&quot;Magic Attribute Setter: Update the Config Object at the Same Time.&quot;&quot;&quot;<\/span>\n        <span class=\"bp\">self<\/span><span class=\"o\">.<\/span><span class=\"vm\">__dict__<\/span><span class=\"p\">[<\/span><span class=\"n\">name<\/span><span class=\"p\">]<\/span> <span class=\"o\">=<\/span> <span class=\"n\">value<\/span>\n        <span class=\"bp\">self<\/span><span class=\"o\">.<\/span><span class=\"n\">_config<\/span><span class=\"o\">.<\/span><span class=\"n\">update<\/span><span class=\"p\">(<\/span><span class=\"n\">name<\/span><span class=\"p\">,<\/span> <span class=\"n\">value<\/span><span class=\"p\">)<\/span>\n\n    <span class=\"k\">def<\/span> <span class=\"nf\">_do_load<\/span><span class=\"p\">(<\/span><span class=\"bp\">self<\/span><span class=\"p\">):<\/span>\n        <span class=\"c1\"># Load Class Attributes<\/span>\n        <span class=\"k\">for<\/span> <span class=\"n\">_<\/span><span class=\"p\">,<\/span> <span class=\"n\">data<\/span> <span class=\"ow\">in<\/span> <span class=\"bp\">self<\/span><span class=\"o\">.<\/span><span class=\"n\">_config<\/span><span class=\"o\">.<\/span><span class=\"n\">config<\/span><span class=\"o\">.<\/span><span class=\"n\">items<\/span><span class=\"p\">():<\/span>\n            <span class=\"k\">for<\/span> <span class=\"n\">key<\/span><span class=\"p\">,<\/span> <span class=\"n\">value<\/span> <span class=\"ow\">in<\/span> <span class=\"n\">data<\/span><span class=\"o\">.<\/span><span class=\"n\">items<\/span><span class=\"p\">():<\/span>\n                <span class=\"bp\">self<\/span><span class=\"o\">.<\/span><span class=\"vm\">__dict__<\/span><span class=\"p\">[<\/span><span class=\"n\">key<\/span><span class=\"p\">]<\/span> <span class=\"o\">=<\/span> <span class=\"n\">value<\/span>\n\n\n<span class=\"k\">class<\/span> <span class=\"nc\">ExampleConfiguration<\/span><span class=\"p\">(<\/span><span class=\"n\">BaseConfig<\/span><span class=\"p\">):<\/span>\n<span class=\"w\">    <\/span><span class=\"sd\">&quot;&quot;&quot;<\/span>\n<span class=\"sd\">    An Example Configuration to Demonstrate the TOML Config Paradigm.<\/span>\n<span class=\"sd\">    &quot;&quot;&quot;<\/span>\n    <span class=\"c1\"># Generic Web-Server Parameters<\/span>\n    <span class=\"n\">host<\/span><span class=\"p\">:<\/span> <span class=\"nb\">str<\/span>\n    <span class=\"n\">port<\/span><span class=\"p\">:<\/span> <span class=\"nb\">int<\/span>\n    <span class=\"c1\"># Another Section<\/span>\n    <span class=\"n\">clients<\/span><span class=\"p\">:<\/span> <span class=\"n\">List<\/span><span class=\"p\">[<\/span><span class=\"nb\">str<\/span><span class=\"p\">]<\/span>\n\n    <span class=\"k\">def<\/span> <span class=\"fm\">__init__<\/span><span class=\"p\">(<\/span><span class=\"bp\">self<\/span><span class=\"p\">,<\/span> <span class=\"n\">config_path<\/span><span class=\"p\">:<\/span> <span class=\"nb\">str<\/span><span class=\"p\">):<\/span>\n<span class=\"w\">        <\/span><span class=\"sd\">&quot;&quot;&quot;Construct the Demonstration Configuration.&quot;&quot;&quot;<\/span>\n        <span class=\"n\">pathlib<\/span><span class=\"o\">.<\/span><span class=\"n\">Path<\/span><span class=\"p\">(<\/span><span class=\"n\">config_path<\/span><span class=\"p\">)<\/span><span class=\"o\">.<\/span><span class=\"n\">parent<\/span><span class=\"o\">.<\/span><span class=\"n\">mkdir<\/span><span class=\"p\">(<\/span><span class=\"n\">parents<\/span><span class=\"o\">=<\/span><span class=\"kc\">True<\/span><span class=\"p\">,<\/span> <span class=\"n\">exist_ok<\/span><span class=\"o\">=<\/span><span class=\"kc\">True<\/span><span class=\"p\">)<\/span>\n        <span class=\"bp\">self<\/span><span class=\"o\">.<\/span><span class=\"n\">_config<\/span> <span class=\"o\">=<\/span> <span class=\"n\">Config<\/span><span class=\"p\">(<\/span><span class=\"n\">config_path<\/span><span class=\"p\">)<\/span>\n        <span class=\"c1\"># Generic Web-Server Settings<\/span>\n        <span class=\"bp\">self<\/span><span class=\"o\">.<\/span><span class=\"n\">_config<\/span><span class=\"o\">.<\/span><span class=\"n\">add_section<\/span><span class=\"p\">(<\/span><span class=\"s1\">&#39;WebApp&#39;<\/span><span class=\"p\">)<\/span><span class=\"o\">.<\/span><span class=\"n\">set<\/span><span class=\"p\">(<\/span>\n            <span class=\"n\">host<\/span><span class=\"o\">=<\/span><span class=\"n\">os<\/span><span class=\"o\">.<\/span><span class=\"n\">getenv<\/span><span class=\"p\">(<\/span><span class=\"s2\">&quot;WEB_HOST&quot;<\/span><span class=\"p\">,<\/span> <span class=\"s2\">&quot;127.0.0.1&quot;<\/span><span class=\"p\">),<\/span>\n            <span class=\"n\">port<\/span><span class=\"o\">=<\/span><span class=\"nb\">int<\/span><span class=\"p\">(<\/span><span class=\"n\">os<\/span><span class=\"o\">.<\/span><span class=\"n\">getenv<\/span><span class=\"p\">(<\/span><span class=\"s2\">&quot;WEB_PORT&quot;<\/span><span class=\"p\">,<\/span> <span class=\"s2\">&quot;8080&quot;<\/span><span class=\"p\">)),<\/span>\n        <span class=\"p\">)<\/span>\n        <span class=\"c1\"># Another Section of Settings<\/span>\n        <span class=\"bp\">self<\/span><span class=\"o\">.<\/span><span class=\"n\">_config<\/span><span class=\"o\">.<\/span><span class=\"n\">add_section<\/span><span class=\"p\">(<\/span><span class=\"s1\">&#39;Clients&#39;<\/span><span class=\"p\">)<\/span><span class=\"o\">.<\/span><span class=\"n\">set<\/span><span class=\"p\">(<\/span>\n            <span class=\"n\">clients<\/span><span class=\"o\">=<\/span><span class=\"n\">os<\/span><span class=\"o\">.<\/span><span class=\"n\">getenv<\/span><span class=\"p\">(<\/span><span class=\"s2\">&quot;CLIENTS&quot;<\/span><span class=\"p\">,<\/span> <span class=\"s2\">&quot;&quot;<\/span><span class=\"p\">)<\/span><span class=\"o\">.<\/span><span class=\"n\">split<\/span><span class=\"p\">(<\/span><span class=\"s1\">&#39;,&#39;<\/span><span class=\"p\">)<\/span>\n        <span class=\"p\">)<\/span>\n        <span class=\"c1\"># Populate the Class Variables<\/span>\n        <span class=\"bp\">self<\/span><span class=\"o\">.<\/span><span class=\"n\">_do_load<\/span><span class=\"p\">()<\/span>\n<\/code><\/pre><\/div>\n\n<p>There's a lot to that sample of code, so let me break it down a bit.<\/p>\n<h3>Monkey-Patch an Update Method into the <code>Config<\/code> Class<\/h3>\n<p>Alright, so this isn't <em>entirely<\/em> necessary, but I find it to be extremely useful. Furthermore, it isn't entirely necessary to add the monkey-patch\nbecause I've <a href=\"https:\/\/github.com\/SemenovAV\/toml_config\/pull\/1\">successfully merged a pull-request<\/a> into the <code>toml_config<\/code> project that provides this\nsame functionality, directly. That means that it's not entirely necessary to use this monkey-patch, yourself.<\/p>\n<div class=\"highlight\"><pre><span><\/span><code><span class=\"c1\"># Inject helper method to simplify modifying values on the fly.<\/span>\n<span class=\"k\">def<\/span> <span class=\"nf\">update<\/span><span class=\"p\">(<\/span><span class=\"bp\">self<\/span><span class=\"p\">:<\/span> <span class=\"n\">Config<\/span><span class=\"p\">,<\/span> <span class=\"n\">key_name<\/span><span class=\"p\">:<\/span> <span class=\"nb\">str<\/span><span class=\"p\">,<\/span> <span class=\"n\">value<\/span><span class=\"p\">:<\/span> <span class=\"nb\">str<\/span><span class=\"p\">):<\/span>\n<span class=\"w\">    <\/span><span class=\"sd\">&quot;&quot;&quot;Update the Specified Key Name - Section Independent.&quot;&quot;&quot;<\/span>\n    <span class=\"k\">for<\/span> <span class=\"n\">section<\/span><span class=\"p\">,<\/span> <span class=\"n\">data<\/span> <span class=\"ow\">in<\/span> <span class=\"bp\">self<\/span><span class=\"o\">.<\/span><span class=\"n\">config<\/span><span class=\"o\">.<\/span><span class=\"n\">items<\/span><span class=\"p\">():<\/span>\n        <span class=\"k\">if<\/span> <span class=\"n\">key_name<\/span> <span class=\"ow\">in<\/span> <span class=\"nb\">list<\/span><span class=\"p\">(<\/span><span class=\"n\">data<\/span><span class=\"o\">.<\/span><span class=\"n\">keys<\/span><span class=\"p\">()):<\/span>\n            <span class=\"bp\">self<\/span><span class=\"o\">.<\/span><span class=\"n\">get_section<\/span><span class=\"p\">(<\/span><span class=\"n\">section<\/span><span class=\"p\">)<\/span>\n            <span class=\"bp\">self<\/span><span class=\"o\">.<\/span><span class=\"n\">set<\/span><span class=\"p\">(<\/span><span class=\"o\">**<\/span><span class=\"p\">{<\/span><span class=\"n\">key_name<\/span><span class=\"p\">:<\/span> <span class=\"n\">value<\/span><span class=\"p\">})<\/span>\n<span class=\"n\">Config<\/span><span class=\"o\">.<\/span><span class=\"n\">update<\/span> <span class=\"o\">=<\/span> <span class=\"n\">update<\/span>\n<\/code><\/pre><\/div>\n\n<p>What this really does for us, is it provides a convenient mechanism to update values in the config on-the-fly and with relative ease. What's more,\nis that it allows us to do a little magic of our own to make attributes a bit more <em>magic<\/em>.<\/p>\n<h3>Making Configuration Attributes <em>MAGIC<\/em><\/h3>\n<p>I'm using this pattern with some high-school students, so I really wanted to impress upon them just how \"magic\" and easy some things can be in a\nsolid, modern language like Python. So, I spent some time figuring out how I could make it such that the configuration class would support some\nintelligent attribute updates, and save the configuration file when the attributes are applied. To make that happen, and to make it possible to\nbuild upon the framework extensibly, I built a base class.<\/p>\n<div class=\"highlight\"><pre><span><\/span><code><span class=\"k\">class<\/span> <span class=\"nc\">BaseConfig<\/span><span class=\"p\">(<\/span><span class=\"nb\">object<\/span><span class=\"p\">):<\/span>\n<span class=\"w\">    <\/span><span class=\"sd\">&quot;&quot;&quot;Base Configuration Object: Used for Inheritance for Additional Config.&quot;&quot;&quot;<\/span>\n    <span class=\"n\">_config<\/span><span class=\"p\">:<\/span> <span class=\"n\">Config<\/span>\n\n    <span class=\"nd\">@property<\/span>\n    <span class=\"k\">def<\/span> <span class=\"nf\">config<\/span><span class=\"p\">(<\/span><span class=\"bp\">self<\/span><span class=\"p\">):<\/span>\n<span class=\"w\">        <\/span><span class=\"sd\">&quot;&quot;&quot;Return the Full Configuration.&quot;&quot;&quot;<\/span>\n        <span class=\"k\">return<\/span> <span class=\"bp\">self<\/span><span class=\"o\">.<\/span><span class=\"n\">_config<\/span><span class=\"o\">.<\/span><span class=\"n\">config<\/span>\n\n    <span class=\"c1\"># THIS IS THE IMPORTANT PART, RIGHT HERE!!!<\/span>\n    <span class=\"k\">def<\/span> <span class=\"fm\">__setattr__<\/span><span class=\"p\">(<\/span><span class=\"bp\">self<\/span><span class=\"p\">,<\/span> <span class=\"n\">name<\/span><span class=\"p\">:<\/span> <span class=\"nb\">str<\/span><span class=\"p\">,<\/span> <span class=\"n\">value<\/span><span class=\"p\">:<\/span> <span class=\"n\">Any<\/span><span class=\"p\">)<\/span> <span class=\"o\">-&gt;<\/span> <span class=\"kc\">None<\/span><span class=\"p\">:<\/span>\n<span class=\"w\">        <\/span><span class=\"sd\">&quot;&quot;&quot;Magic Attribute Setter: Update the Config Object at the Same Time.&quot;&quot;&quot;<\/span>\n        <span class=\"bp\">self<\/span><span class=\"o\">.<\/span><span class=\"vm\">__dict__<\/span><span class=\"p\">[<\/span><span class=\"n\">name<\/span><span class=\"p\">]<\/span> <span class=\"o\">=<\/span> <span class=\"n\">value<\/span>\n        <span class=\"bp\">self<\/span><span class=\"o\">.<\/span><span class=\"n\">_config<\/span><span class=\"o\">.<\/span><span class=\"n\">update<\/span><span class=\"p\">(<\/span><span class=\"n\">name<\/span><span class=\"p\">,<\/span> <span class=\"n\">value<\/span><span class=\"p\">)<\/span>\n\n    <span class=\"k\">def<\/span> <span class=\"nf\">_do_load<\/span><span class=\"p\">(<\/span><span class=\"bp\">self<\/span><span class=\"p\">):<\/span>\n        <span class=\"c1\"># Load Class Attributes<\/span>\n        <span class=\"k\">for<\/span> <span class=\"n\">_<\/span><span class=\"p\">,<\/span> <span class=\"n\">data<\/span> <span class=\"ow\">in<\/span> <span class=\"bp\">self<\/span><span class=\"o\">.<\/span><span class=\"n\">_config<\/span><span class=\"o\">.<\/span><span class=\"n\">config<\/span><span class=\"o\">.<\/span><span class=\"n\">items<\/span><span class=\"p\">():<\/span>\n            <span class=\"k\">for<\/span> <span class=\"n\">key<\/span><span class=\"p\">,<\/span> <span class=\"n\">value<\/span> <span class=\"ow\">in<\/span> <span class=\"n\">data<\/span><span class=\"o\">.<\/span><span class=\"n\">items<\/span><span class=\"p\">():<\/span>\n                <span class=\"bp\">self<\/span><span class=\"o\">.<\/span><span class=\"vm\">__dict__<\/span><span class=\"p\">[<\/span><span class=\"n\">key<\/span><span class=\"p\">]<\/span> <span class=\"o\">=<\/span> <span class=\"n\">value<\/span>\n<\/code><\/pre><\/div>\n\n<p>The real magic here comes from the use of the Python <em>magic-method<\/em>: <code>__setattr__<\/code>. This method is called when an attribute is modified, and\nallows me to do some fun things. Namely when I update a configuration value such as:<\/p>\n<div class=\"highlight\"><pre><span><\/span><code><span class=\"o\">&gt;&gt;&gt;<\/span> <span class=\"n\">my_config<\/span> <span class=\"o\">=<\/span> <span class=\"n\">ExampleConfiguration<\/span><span class=\"p\">(<\/span><span class=\"s2\">&quot;path\/to\/config.toml&quot;<\/span><span class=\"p\">)<\/span>\n<span class=\"o\">&gt;&gt;&gt;<\/span> <span class=\"n\">my_config<\/span><span class=\"o\">.<\/span><span class=\"n\">port<\/span>\n<span class=\"mi\">8080<\/span>\n<span class=\"o\">&gt;&gt;&gt;<\/span> <span class=\"n\">my_config<\/span><span class=\"o\">.<\/span><span class=\"n\">port<\/span> <span class=\"o\">=<\/span> <span class=\"mi\">5050<\/span> <span class=\"c1\"># This will change the value, and modify the config file<\/span>\n<span class=\"o\">&gt;&gt;&gt;<\/span> <span class=\"n\">my_config<\/span><span class=\"o\">.<\/span><span class=\"n\">port<\/span>\n<span class=\"mi\">5050<\/span>\n<\/code><\/pre><\/div>\n\n<p>The configuration will magically apply the change <em>and<\/em> update the configuration file, just to make sure everything's set!<\/p>\n<blockquote>\n<p>Marvelous!<\/p>\n<\/blockquote>\n<h3>Pre-Loading the Data<\/h3>\n<p>Like I mentioned earlier, I want this thing to be somewhat intelligent, allowing me to set environment variables that can pre-load data for me so\nthat I don't have to deal with constructing the original TOML file, if I don't want to. And let's be honest. I'm lazy, I don't want to.\nBut setting this up is easy. I just use <code>os.getenv<\/code> to retrieve the necessary values, and use those as defaults for the config file!<\/p>\n<div class=\"highlight\"><pre><span><\/span><code><span class=\"k\">class<\/span> <span class=\"nc\">ExampleConfiguration<\/span><span class=\"p\">(<\/span><span class=\"n\">BaseConfig<\/span><span class=\"p\">):<\/span>\n<span class=\"w\">    <\/span><span class=\"sd\">&quot;&quot;&quot;<\/span>\n<span class=\"sd\">    An Example Configuration to Demonstrate the TOML Config Paradigm.<\/span>\n<span class=\"sd\">    &quot;&quot;&quot;<\/span>\n    <span class=\"c1\"># Generic Web-Server Parameters<\/span>\n    <span class=\"n\">host<\/span><span class=\"p\">:<\/span> <span class=\"nb\">str<\/span>\n    <span class=\"n\">port<\/span><span class=\"p\">:<\/span> <span class=\"nb\">int<\/span>\n    <span class=\"c1\"># Another Section<\/span>\n    <span class=\"n\">clients<\/span><span class=\"p\">:<\/span> <span class=\"n\">List<\/span><span class=\"p\">[<\/span><span class=\"nb\">str<\/span><span class=\"p\">]<\/span>\n\n    <span class=\"k\">def<\/span> <span class=\"fm\">__init__<\/span><span class=\"p\">(<\/span><span class=\"bp\">self<\/span><span class=\"p\">,<\/span> <span class=\"n\">config_path<\/span><span class=\"p\">:<\/span> <span class=\"nb\">str<\/span><span class=\"p\">):<\/span>\n<span class=\"w\">        <\/span><span class=\"sd\">&quot;&quot;&quot;Construct the Demonstration Configuration.&quot;&quot;&quot;<\/span>\n        <span class=\"n\">pathlib<\/span><span class=\"o\">.<\/span><span class=\"n\">Path<\/span><span class=\"p\">(<\/span><span class=\"n\">config_path<\/span><span class=\"p\">)<\/span><span class=\"o\">.<\/span><span class=\"n\">parent<\/span><span class=\"o\">.<\/span><span class=\"n\">mkdir<\/span><span class=\"p\">(<\/span><span class=\"n\">parents<\/span><span class=\"o\">=<\/span><span class=\"kc\">True<\/span><span class=\"p\">,<\/span> <span class=\"n\">exist_ok<\/span><span class=\"o\">=<\/span><span class=\"kc\">True<\/span><span class=\"p\">)<\/span>\n        <span class=\"bp\">self<\/span><span class=\"o\">.<\/span><span class=\"n\">_config<\/span> <span class=\"o\">=<\/span> <span class=\"n\">Config<\/span><span class=\"p\">(<\/span><span class=\"n\">config_path<\/span><span class=\"p\">)<\/span>\n        <span class=\"c1\"># Generic Web-Server Settings<\/span>\n        <span class=\"bp\">self<\/span><span class=\"o\">.<\/span><span class=\"n\">_config<\/span><span class=\"o\">.<\/span><span class=\"n\">add_section<\/span><span class=\"p\">(<\/span><span class=\"s1\">&#39;WebApp&#39;<\/span><span class=\"p\">)<\/span><span class=\"o\">.<\/span><span class=\"n\">set<\/span><span class=\"p\">(<\/span>\n            <span class=\"n\">host<\/span><span class=\"o\">=<\/span><span class=\"n\">os<\/span><span class=\"o\">.<\/span><span class=\"n\">getenv<\/span><span class=\"p\">(<\/span><span class=\"s2\">&quot;WEB_HOST&quot;<\/span><span class=\"p\">,<\/span> <span class=\"s2\">&quot;127.0.0.1&quot;<\/span><span class=\"p\">),<\/span>\n            <span class=\"n\">port<\/span><span class=\"o\">=<\/span><span class=\"nb\">int<\/span><span class=\"p\">(<\/span><span class=\"n\">os<\/span><span class=\"o\">.<\/span><span class=\"n\">getenv<\/span><span class=\"p\">(<\/span><span class=\"s2\">&quot;WEB_PORT&quot;<\/span><span class=\"p\">,<\/span> <span class=\"s2\">&quot;8080&quot;<\/span><span class=\"p\">)),<\/span>\n        <span class=\"p\">)<\/span>\n        <span class=\"c1\"># Another Section of Settings<\/span>\n        <span class=\"bp\">self<\/span><span class=\"o\">.<\/span><span class=\"n\">_config<\/span><span class=\"o\">.<\/span><span class=\"n\">add_section<\/span><span class=\"p\">(<\/span><span class=\"s1\">&#39;Clients&#39;<\/span><span class=\"p\">)<\/span><span class=\"o\">.<\/span><span class=\"n\">set<\/span><span class=\"p\">(<\/span>\n            <span class=\"n\">clients<\/span><span class=\"o\">=<\/span><span class=\"n\">os<\/span><span class=\"o\">.<\/span><span class=\"n\">getenv<\/span><span class=\"p\">(<\/span><span class=\"s2\">&quot;CLIENTS&quot;<\/span><span class=\"p\">,<\/span> <span class=\"s2\">&quot;&quot;<\/span><span class=\"p\">)<\/span><span class=\"o\">.<\/span><span class=\"n\">split<\/span><span class=\"p\">(<\/span><span class=\"s1\">&#39;,&#39;<\/span><span class=\"p\">)<\/span>\n        <span class=\"p\">)<\/span>\n        <span class=\"c1\"># Populate the Class Variables<\/span>\n        <span class=\"bp\">self<\/span><span class=\"o\">.<\/span><span class=\"n\">_do_load<\/span><span class=\"p\">()<\/span>\n<\/code><\/pre><\/div>\n\n<p>See in that little snippet, I use <code>Config<\/code>'s system of adding sections with their respective names, then I set the data for each of the fields\ncontained within each section. Namely, here there's two sections: <code>WebApp<\/code> and <code>Clients<\/code>. For each value in those sections, I use <code>os.getenv<\/code>\nto pull in the appropriate initialization value, or fall back to a default if no such environment variable exists.<\/p>\n<h2>Closing Thoughts<\/h2>\n<p>I think this is a pretty simple, and convenient code-pattern to support configuration from environment variables and from TOML, while at the\nsame time, providing a convenient update mechanism. This isn't as secure, or as robust as something with a database might be. After all, it's\nentirely possible for on-disk-data to be corrupted because of improper shutdown during the data write; however unlikely that may be.<\/p>\n<p>Either way, it's simple, convenient, and I enjoy it!<\/p>\n<p>Happy coding!<\/p>","category":[{"@attributes":{"term":"Python"}},{"@attributes":{"term":"python"}},{"@attributes":{"term":"toml"}},{"@attributes":{"term":"configuration"}},{"@attributes":{"term":"development"}},{"@attributes":{"term":"environment-variables"}},{"@attributes":{"term":"dot-files"}}]},{"title":"Packaging Single File Python Projects","link":{"@attributes":{"href":"https:\/\/blog.stanleysolutionsnw.com\/packaging-single-file-python-projects.html","rel":"alternate"}},"published":"2023-01-16T09:50:00-08:00","updated":"2023-01-16T09:50:00-08:00","author":{"name":"Joe Stanley"},"id":"tag:blog.stanleysolutionsnw.com,2023-01-16:\/packaging-single-file-python-projects.html","summary":"<p>Somehow, I've managed to build quite a few random Python package, and contribute to others. I've recently been working towards converting all of the Python projects I manage to the new pyproject.toml standards for packaging, and I've recently had to work through an interesting little challenge for some of the projects which only contain a sigle Python file. No module folder, no <code>__init__.py<\/code>. Just a single file.<\/p>","content":"<p>So... I somehow seem to have become a maintainer for a number of projects. I don't claim to be a <em>good<\/em> maintainer. Just that I am a maintainer. After all,\nI'm involved, in one way, or another with each of the following projects.<\/p>\n<ul>\n<li><a href=\"https:\/\/github.com\/engineerjoe440\/ElectricPy\"><code>ElectricPy<\/code><\/a> -- I'm the Primary maintainer for this one, after all, I was the original author.<\/li>\n<li><a href=\"https:\/\/github.com\/engineerjoe440\/pycev\"><code>PyCEV<\/code><\/a> -- Again... Primary maintainer.<\/li>\n<li><a href=\"https:\/\/github.com\/engineerjoe440\/python-comtrade\"><code>Python-COMTRADE<\/code><\/a> -- Ok... so this is my fork of the original project. I'm not <em>quite<\/em> that clever to put this one together by myself.<\/li>\n<li><a href=\"https:\/\/github.com\/engineerjoe440\/selprotopy\"><code>SELProtoPy<\/code><\/a> -- Sole Maintainer.<\/li>\n<li><a href=\"https:\/\/github.com\/engineerjoe440\/schemdraw-markdown\"><code>Schemdraw-Markdown<\/code><\/a> -- Sole Maintainer.<\/li>\n<\/ul>\n<p>Still, even <em>questionable<\/em> maintainers, such as myself, can exercise some good practices when it comes to package management.<\/p>\n<h2>What is <code>pyproject.toml<\/code> and why do we care?<\/h2>\n<p>Good question.<\/p>\n<p>It's one that lots of people have asked. So... I've put together a list of quick \"finds\" when I searched for <em>\"why to move from setup.py to pyproject.toml\"<\/em> on Google.<\/p>\n<ul>\n<li>http:\/\/ivory.idyll.org\/blog\/2021-transition-to-pyproject.toml-example.html<\/li>\n<li>https:\/\/stackoverflow.com\/questions\/72352801\/migration-from-setup-py-to-pyproject-toml-how-to-specify-package-name<\/li>\n<li>https:\/\/ianhopkinson.org.uk\/2022\/02\/understanding-setup-py-setup-cfg-and-pyproject-toml-in-python\/<\/li>\n<li>https:\/\/setuptools.pypa.io\/en\/latest\/userguide\/pyproject_config.html<\/li>\n<\/ul>\n<p>There's plenty of other well-informed articles out there; I just picked the first couple. I'll rehash, though for what its worth.<\/p>\n<p>The <code>setup.py<\/code> file is much what it sounds like. It's an executable Python file which is largely responsible for demarking the particular packaging parameters needed\nfor a Python project. Nothing too crazy about it, but as the industry has grown, it's become increasingly clear that having a package's installation managed in an\nexecutable script is less than ideal.<\/p>\n<p>Along came <code>pyproject.toml<\/code>. Offering all the same great flavors that <code>setup.py<\/code> brought to the table, with half the fat and fewer calories... I mean, without the\nneed for any executable scripts being run during installation. Bingo!<\/p>\n<h2>What's so special about packaging single-file Python projects?<\/h2>\n<p>Well, let's look at a common Python package layout:<\/p>\n<div class=\"highlight\"><pre><span><\/span><code><span class=\"o\">|-<\/span><span class=\"w\"> <\/span><span class=\"nx\">my_package<\/span><span class=\"o\">\/<\/span>\n<span class=\"o\">|<\/span><span class=\"w\">  <\/span><span class=\"o\">|-<\/span><span class=\"w\"> <\/span><span class=\"nx\">__init__<\/span><span class=\"p\">.<\/span><span class=\"nx\">py<\/span>\n<span class=\"o\">|<\/span><span class=\"w\">  <\/span><span class=\"o\">|-<\/span><span class=\"w\"> <\/span><span class=\"nx\">some_other_file<\/span><span class=\"p\">.<\/span><span class=\"nx\">py<\/span>\n<span class=\"o\">|<\/span>\n<span class=\"o\">|-<\/span><span class=\"w\"> <\/span><span class=\"nx\">pyproject<\/span><span class=\"p\">.<\/span><span class=\"nx\">toml<\/span>\n<span class=\"o\">|-<\/span><span class=\"w\"> <\/span><span class=\"nx\">setup<\/span><span class=\"p\">.<\/span><span class=\"nx\">py<\/span>\n<\/code><\/pre><\/div>\n\n<p>See that in this case, the \"package\" is all contained under the <code>my_package\/<\/code> folder, which contains the appropriate <code>__init__.py<\/code> necessary to make the folder work\nas a true Python package.<\/p>\n<p>I want to do something a little <em>different<\/em> though. I mean, let's be honest; is it really all that surprising that I, <em>Joe Stanley<\/em> want to do things <em>differently<\/em>?<\/p>\n<blockquote>\n<p>Nope.<\/p>\n<\/blockquote>\n<hr>\n<p>I want a flat package like the one shown below. A package that only contains a single Python file, because that's all that it needs. No extra bloatware!<\/p>\n<div class=\"highlight\"><pre><span><\/span><code><span class=\"o\">|-<\/span><span class=\"w\"> <\/span><span class=\"nx\">my_package<\/span><span class=\"p\">.<\/span><span class=\"nx\">py<\/span>\n<span class=\"o\">|-<\/span><span class=\"w\"> <\/span><span class=\"nx\">pyproject<\/span><span class=\"p\">.<\/span><span class=\"nx\">toml<\/span>\n<span class=\"o\">|-<\/span><span class=\"w\"> <\/span><span class=\"nx\">setup<\/span><span class=\"p\">.<\/span><span class=\"nx\">py<\/span>\n<\/code><\/pre><\/div>\n\n<blockquote>\n<p>But... How do I do that?<\/p>\n<\/blockquote>\n<h2>Making <code>pyproject.toml<\/code> do my bidding...<\/h2>\n<p>So... after a bit of research on <a href=\"https:\/\/flit.pypa.io\/en\/stable\/pyproject_toml.html?highlight=tool.flit.module#module-section\"><code>flit<\/code>'s documentation<\/a>, and\nfound a nice, concise way of declaring the particular module that's available in the package. See the example below from\n<a href=\"https:\/\/github.com\/engineerjoe440\/python-comtrade\/blob\/master\/pyproject.toml\">my fork of <code>python-comtrade<\/code><\/a><\/p>\n<div class=\"highlight\"><pre><span><\/span><code><span class=\"k\">[build-system]<\/span>\n<span class=\"n\">requires<\/span><span class=\"w\"> <\/span><span class=\"o\">=<\/span><span class=\"w\"> <\/span><span class=\"p\">[<\/span><span class=\"s2\">&quot;flit_core &gt;=3.2,&lt;4&quot;<\/span><span class=\"p\">]<\/span>\n<span class=\"n\">build-backend<\/span><span class=\"w\"> <\/span><span class=\"o\">=<\/span><span class=\"w\"> <\/span><span class=\"s2\">&quot;flit_core.buildapi&quot;<\/span>\n\n<span class=\"k\">[project]<\/span>\n<span class=\"n\">name<\/span><span class=\"w\"> <\/span><span class=\"o\">=<\/span><span class=\"w\"> <\/span><span class=\"s2\">&quot;python-comtrade&quot;<\/span>\n<span class=\"n\">authors<\/span><span class=\"w\"> <\/span><span class=\"o\">=<\/span><span class=\"w\"> <\/span><span class=\"p\">[<\/span>\n<span class=\"w\">    <\/span><span class=\"p\">{<\/span><span class=\"n\">name<\/span><span class=\"w\"> <\/span><span class=\"p\">=<\/span><span class=\"w\"> <\/span><span class=\"s2\">&quot;David Parrini&quot;<\/span><span class=\"p\">},<\/span>\n<span class=\"w\">    <\/span><span class=\"p\">{<\/span><span class=\"n\">name<\/span><span class=\"w\"> <\/span><span class=\"p\">=<\/span><span class=\"w\"> <\/span><span class=\"s2\">&quot;Joe Stanley&quot;<\/span><span class=\"p\">,<\/span><span class=\"w\"> <\/span><span class=\"n\">email<\/span><span class=\"w\"> <\/span><span class=\"p\">=<\/span><span class=\"w\"> <\/span><span class=\"s2\">&quot;engineerjoe440@yahoo.com&quot;<\/span><span class=\"p\">}<\/span>\n<span class=\"p\">]<\/span>\n<span class=\"n\">maintainers<\/span><span class=\"w\"> <\/span><span class=\"o\">=<\/span><span class=\"w\"> <\/span><span class=\"p\">[<\/span>\n<span class=\"w\">    <\/span><span class=\"p\">{<\/span><span class=\"n\">name<\/span><span class=\"w\"> <\/span><span class=\"p\">=<\/span><span class=\"w\"> <\/span><span class=\"s2\">&quot;Joe Stanley&quot;<\/span><span class=\"p\">,<\/span><span class=\"w\"> <\/span><span class=\"n\">email<\/span><span class=\"w\"> <\/span><span class=\"p\">=<\/span><span class=\"w\"> <\/span><span class=\"s2\">&quot;engineerjoe440@yahoo.com&quot;<\/span><span class=\"p\">}<\/span>\n<span class=\"p\">]<\/span>\n<span class=\"n\">description<\/span><span class=\"w\"> <\/span><span class=\"o\">=<\/span><span class=\"w\"> <\/span><span class=\"s2\">&quot;A Python 3 module designed to read Common Format for Transient Data Exchange (COMTRADE) files.&quot;<\/span>\n<span class=\"n\">readme<\/span><span class=\"w\"> <\/span><span class=\"o\">=<\/span><span class=\"w\"> <\/span><span class=\"s2\">&quot;README.md&quot;<\/span>\n<span class=\"n\">license<\/span><span class=\"w\"> <\/span><span class=\"o\">=<\/span><span class=\"w\"> <\/span><span class=\"p\">{<\/span><span class=\"n\">file<\/span><span class=\"w\"> <\/span><span class=\"p\">=<\/span><span class=\"w\"> <\/span><span class=\"s2\">&quot;LICENSE&quot;<\/span><span class=\"p\">}<\/span>\n<span class=\"n\">classifiers<\/span><span class=\"w\"> <\/span><span class=\"o\">=<\/span><span class=\"w\"> <\/span><span class=\"p\">[<\/span>\n<span class=\"w\">    <\/span><span class=\"s2\">&quot;License :: OSI Approved :: MIT License&quot;<\/span>\n<span class=\"p\">]<\/span>\n<span class=\"n\">dynamic<\/span><span class=\"w\"> <\/span><span class=\"o\">=<\/span><span class=\"w\"> <\/span><span class=\"p\">[<\/span><span class=\"s2\">&quot;version&quot;<\/span><span class=\"p\">]<\/span>\n\n<span class=\"k\">[project.urls]<\/span>\n<span class=\"n\">Home<\/span><span class=\"w\"> <\/span><span class=\"o\">=<\/span><span class=\"w\"> <\/span><span class=\"s2\">&quot;https:\/\/github.com\/engineerjoe440\/python-comtrade&quot;<\/span>\n\n<span class=\"c1\"># Here&#39;s where the magic happens....<\/span>\n<span class=\"k\">[tool.flit.module]<\/span>\n<span class=\"n\">name<\/span><span class=\"w\"> <\/span><span class=\"o\">=<\/span><span class=\"w\"> <\/span><span class=\"s2\">&quot;comtrade&quot;<\/span>\n<\/code><\/pre><\/div>\n\n<p>And just like that... This package is ready to publish just the one little-ol'-Python-file without any heartache.<\/p>","category":[{"@attributes":{"term":"Python"}},{"@attributes":{"term":"python"}},{"@attributes":{"term":"packaging"}},{"@attributes":{"term":"pyproject.toml"}},{"@attributes":{"term":"pypi"}},{"@attributes":{"term":"development"}},{"@attributes":{"term":"build"}}]},{"title":"Python, Pianobar, and MQTT?","link":{"@attributes":{"href":"https:\/\/blog.stanleysolutionsnw.com\/python-pianobar-and-mqtt.html","rel":"alternate"}},"published":"2023-01-15T13:52:00-08:00","updated":"2023-01-15T13:52:00-08:00","author":{"name":"Joe Stanley"},"id":"tag:blog.stanleysolutionsnw.com,2023-01-15:\/python-pianobar-and-mqtt.html","summary":"<p>We know that I'm something of an audio buff. I love having music around me all the time. But isn't that becoming more of a staple in the American home, anyway? I certainly think so. There's lots of folks who are also listening to music all the time. I have my own personal preferences, though. (Shocker, I know.) The thing is, I want my audio system to tie nicely into my home. I want play\/pause buttons scattered around, and well... I've got more demands.<\/p>","content":"<blockquote>\n<p>Joe? Have Demands?<\/p>\n<\/blockquote>\n<p>Psh! I know... Right? It's surprising... <strong><em>NOT<\/em><\/strong><\/p>\n<p>Anyway... I want to have music streaming in my home, but I want it to meet some requirements:<\/p>\n<ul>\n<li>I want to play Pandora using <a href=\"https:\/\/github.com\/PromyLOPh\/pianobar\">Pianobar<\/a> -- The wonderful Linux-terminal, Pandora internet radio client.<\/li>\n<li>I want to control the application over MQTT<\/li>\n<li>I want to control the application over HTTP<\/li>\n<li>I want the application to be resilient and restart itself if things go... awry<\/li>\n<li>I want the application to give me information about what's playing<\/li>\n<li>I want the application to be built with a modern Python framework<\/li>\n<\/ul>\n<blockquote>\n<p>So... What exists, already?<\/p>\n<\/blockquote>\n<p>Well, there's actually some pretty neat tools already out there:<\/p>\n<ul>\n<li><a href=\"https:\/\/pithos.github.io\/\">Pithos<\/a><\/li>\n<li><a href=\"http:\/\/deviousfish.com\/pianod\/\">Pianod<\/a><\/li>\n<li><a href=\"https:\/\/hackaday.com\/2012\/09\/20\/how-to-build-your-own-dedicated-pandora-radio\/\">Standalone Pandora Player<\/a><\/li>\n<li><a href=\"https:\/\/www.instructables.com\/Pandoras-Box-An-Internet-Radio-player-made-with\/\">Pandora's Box<\/a><\/li>\n<li><a href=\"https:\/\/volumio.com\/en\/\">Volumio<\/a><\/li>\n<\/ul>\n<p>Unfortunately, for one reason or another, none of these really meet my objectives. In fact, I want to go on a rant some other time about why <em>Volumio<\/em> doesn't\nreally meet my needs as a tinkerer\/hacker\/general-nerd-extraordinaire.<\/p>\n<h2>My Solution?<\/h2>\n<p>So glad you asked...<\/p>\n<p>I decided to write my own tool. Because that's <em>just what I need<\/em>. Another project in my life. You know... because of all this free-time I have.<\/p>\n<p>So, this project of mine will leverage the fantastic ecosystem for Pianobar, allowing me to interact with their phenomenal system of control and monitoring,\nand I'll add to that an MQTT and HTTP interface. Both the MQTT and HTTP interfaces will allow simple control, real-time information, and more!<\/p>\n<p>I started this project back on December 26th, and I'm actually quite proud of how far it's come! I really am tailoring this just for me, but at the same\ntime, I'd like for this to be something that others could pick up and play with, so most of the configuration is customizable. Things like what the MQTT\ntopics are, what the MQTT broker uses for an IP address, etc. Those are all things that somebody could pick up and swap out in their own system.<\/p>\n<p>I haven't fully deployed this, quite yet, but that's because I'm still working on getting some of the other Pipewire plumbing in place so that I can\ndeploy it on my home audio server. We'll get there, soon! I'm excited about that!<\/p>\n<h2>Where is this thing stored?<\/h2>\n<p>Well, right now, I've put it all on my GitLab. I'm considering setting it up to push to GitHub automatically, that way I've got some move visible presence\nfor the project as a whole, but you can go visit it here:<\/p>\n<blockquote>\n<p><a href=\"https:\/\/gitlab.stanleysolutionsnw.com\/engineerjoe440\/pianobarplayer\">https:\/\/gitlab.stanleysolutionsnw.com\/engineerjoe440\/pianobarplayer<\/a><\/p>\n<\/blockquote>\n<p>I've picked up some interesting learnings from the project as a whole, but since I've been doing a lot with those same learnings elsewhere, I'll be\nwriting some of my thoughts on configuration with TOML in Python soon!<\/p>","category":[{"@attributes":{"term":"Python"}},{"@attributes":{"term":"python"}},{"@attributes":{"term":"pianobar"}},{"@attributes":{"term":"mqtt"}},{"@attributes":{"term":"http"}},{"@attributes":{"term":"configuration"}},{"@attributes":{"term":"pandora"}},{"@attributes":{"term":"music"}},{"@attributes":{"term":"streaming"}},{"@attributes":{"term":"audio"}},{"@attributes":{"term":"home-automation"}},{"@attributes":{"term":"networking"}}]},{"title":"The Ranch (KRNC) Gets a Face Lift","link":{"@attributes":{"href":"https:\/\/blog.stanleysolutionsnw.com\/the-ranch-gets-a-face-lift.html","rel":"alternate"}},"published":"2023-01-08T12:00:00-08:00","updated":"2023-01-08T12:00:00-08:00","author":{"name":"Joe Stanley"},"id":"tag:blog.stanleysolutionsnw.com,2023-01-08:\/the-ranch-gets-a-face-lift.html","summary":"<p>So, I've got an old (1992) Ford pickup, and it's got a newer-ish stereo. I love keeping my favorite tunes on a USB stick that I can play everything from, but I HATE it when the volume changes from one song to the next. I go from barely being able to hear the music to having my eardrums blown out in 0.5 seconds. So, I've come to the conclusion that I need to fix that. With PYTHON!<\/p>","content":"<p>Like I'd mentioned; I absolutely <em>HATE<\/em> it when the audio in my car goes from practically inaudible to raising the dead.<\/p>\n<blockquote>\n<p>Not fun.<\/p>\n<\/blockquote>\n<p>So the question becomes, how do I do something about this problem of mine? What can I do to equalize the volume levels?\nWell, of course I can use an application to compress and normalize the volume of tracks. It's something I was quite\naccustomed to from my old radio days. You see, that's pretty common to do for a radio-station's imaging\/branding tracks.\nYou know... the little quips you hear between songs like \"Your favorite station for the 90's through now!\" etc. Most of\nthose type of track go through some kind of compressor prior to be loaded into the on-air playback system, and what's\nmore, most stations use a real-time compressor that takes the fully-mixed audio stream from the on-air studio and\ncompresses that for going out on the air. That's twice the compression for those of you who are counting, but normally\nthe compression used for the imaging tracks is specifically customized such that it will \"play nicely\" with the in-line\ncompressor in the studio.<\/p>\n<p>Ok, so that was a diversion from the root of this conversation, but I hope it gives you some of the background to\nunderstand the next bit of this topic.<\/p>\n<hr>\n<p>Compression is a slick way of bringing the low-volume stuff up to make sure it's audible, and bringing the loud stuff\ndown a bit, without making the whole thing sound terrible. I found a GIF that does its best to show some of this. It's\nnot great, but I couldn't be bothered to make my own. Hah!<\/p>\n<p><img src=\"https:\/\/audiophilestyle.com\/uploads\/monthly_2019_08\/510421229_GrundmanvsDownloadWavforms.gif.2fb2cdf545abce8f61fdc881d4ac9db0.gif\" width=\"100%\" alt=\"compressing audio\"><\/p>\n<p>There's a number of compressors available, and they're all a bit touchy, but there's some really cool ways to do it with\nLinux command-line utilities. Namely <a href=\"https:\/\/ffmpeg.org\/\"><code>ffmpeg<\/code><\/a> -- the universal, quintessential audio utility.<\/p>\n<p>If there's an app on Linux or Windows that works with audio, it almost certainly has some compatibility or reliance on\nffmpeg.<\/p>\n<p>With ffmpeg, I can do lots of things with audio... basically whatever I wanted to... but to name a few things:<\/p>\n<ul>\n<li>compress<\/li>\n<li>normalize<\/li>\n<li>filter<\/li>\n<li>equalize<\/li>\n<li>and much, <em>much<\/em> more...<\/li>\n<\/ul>\n<p>So, I did some digital spelunking and found a handful of little sets of arguments I liked for some general filters:<\/p>\n<table>\n<thead>\n<tr>\n<th>What I call it<\/th>\n<th>What the command is<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>\"default\"<\/td>\n<td><code>-filter_complex compand=attacks=0:points=-80\/-900\\|-45\/-15\\|-27\/-9\\|0\/-7\\|20\/-7:gain=5<\/code><\/td>\n<\/tr>\n<tr>\n<td>\"normalize\"<\/td>\n<td><code>-filter:a \"dynaudnorm\"<\/code><\/td>\n<\/tr>\n<tr>\n<td>\"light\"<\/td>\n<td><code>-filter:a compand=.3\\|.3:1\\|1:-90\/-60\\|-60\/-40\\|-40\/-30\\|-20\/-20:6:0:-90:0.2<\/code><\/td>\n<\/tr>\n<tr>\n<td>\"heavy\"<\/td>\n<td><code>-filter:a compand=0\\|0:1\\|1:-90\/-900\\|-70\/-70\\|-30\/-9\\|0\/-3:6:0:0:0<\/code><\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>So clearly, I've got a good little set of compressors and filters. Still, would you remember any of those commands? I\ncertainly won't. So I'd like to have a little tool do that for me.<\/p>\n<p>At the same time, I've also been wanting to try out the new Python tool <a href=\"https:\/\/flet.dev\/\"><code>Flet<\/code><\/a> for building Flutter\nbased applications. No, I don't want to write Dart applications, but I wouldn't mind if there's a layer between me and\nthat mess.<\/p>\n<p>So... I got to work and rebuilt my\n<a href=\"https:\/\/gitlab.stanleysolutionsnw.com\/krnc\/usb-manager\">\"Universal Song Barn Manager\" Application<\/a>. Do you notice the\nplay-on-words in the name? <em>USB<\/em> Manager...<\/p>\n<p><img src=\"https:\/\/gitlab.stanleysolutionsnw.com\/krnc\/usb-manager\/-\/raw\/master\/images\/SongBarn.png\" width=\"100%\" alt=\"KRNC - Universal Song Barn Manager\"><\/p>\n<p>I want to toot my own horn here a little bit. I built this app in <em>one, single day<\/em>.<\/p>\n<p>Admittedly, I went back and have touched up things in subsequent days, but the original application was written in one\nday. I'm kinda proud of that.<\/p>\n<p>So, the application has a relatively simple workflow:<\/p>\n<ol>\n<li>Open app<\/li>\n<li>Add songs to your \"barn\" (record file) with the app's \"<code>+<\/code>\" button<\/li>\n<li>Change the filtering selection as desired, change the track's \"Title\" if needed<\/li>\n<li>Select the USB drive you want to store the files on<\/li>\n<li>Load the drive<\/li>\n<li>Listen and enjoy!<\/li>\n<\/ol>\n<hr>\n<p>Now... I've got some work left to do. I still want to get the app set up so that it can use a \"Saddle Bag\" system to\nsupport retrieving short audio clips as \"radio imaging\" because I'm just that kind of nerd. That'll have to come soon.<\/p>\n<p>Take a look at the GitLab information if you'd like. Let me know what you think, and feel free to use it for your own\nneeds if you'd like!<\/p>","category":[{"@attributes":{"term":"Python"}},{"@attributes":{"term":"linux"}},{"@attributes":{"term":"audio"}},{"@attributes":{"term":"python"}},{"@attributes":{"term":"ffmpeg"}},{"@attributes":{"term":"sound"}},{"@attributes":{"term":"terminal"}},{"@attributes":{"term":"command-line"}},{"@attributes":{"term":"flet"}},{"@attributes":{"term":"flutter"}},{"@attributes":{"term":"application"}},{"@attributes":{"term":"krnc"}}]},{"title":"My Way of Intalling Python on Windows in 2022","link":{"@attributes":{"href":"https:\/\/blog.stanleysolutionsnw.com\/my-way-of-installing-python-on-windows-in-2022.html","rel":"alternate"}},"published":"2022-11-02T18:41:00-07:00","updated":"2022-11-02T18:41:00-07:00","author":{"name":"Joe Stanley"},"id":"tag:blog.stanleysolutionsnw.com,2022-11-02:\/my-way-of-installing-python-on-windows-in-2022.html","summary":"<p>Finally! Python 3.11 is out! It's new, it's fast(er than previous Python versions), and it's got some dandy new features. And if you wanted to know how I go about putting it on a Windows machine, let me show you...<\/p>","content":"<p>Python is a bit of a tricky subject on Windows, and that's why I've developed my own \"best practice\" for installing it on my systems. Let me briefly walk you through the steps.<\/p>\n<h2>1) Download the latest version of Python<\/h2>\n<p><img src=\"https:\/\/blog.stanleysolutionsnw.com\/2022-11-02_16-36-36.png\" style=\"width: 100%\" alt=\"Step 1 - Download the Latest Python\"><\/p>\n<h2>2) Run the installer<\/h2>\n<blockquote>\n<p>but make sure you check \"Add Python to Path\" and use the \"Customize\" option for installation<\/p>\n<\/blockquote>\n<p><img src=\"https:\/\/blog.stanleysolutionsnw.com\/2022-11-02_16-37-12.png\" style=\"width: 100%\" alt=\"Step 2 - Run the Installer\"><\/p>\n<h2>3) Select Everything!<\/h2>\n<p><img src=\"https:\/\/blog.stanleysolutionsnw.com\/2022-11-02_16-37-41.png\" style=\"width: 100%\" alt=\"Step 3 - Select Everything\"><\/p>\n<h2>4) Select \"Install for All Users\" and Customize the Installation Path<\/h2>\n<p>This will make sure that Python is installed in a simple, and accessible place. I find it VERY helpful to have Python rooted at the <code>C:\\<\/code> drive level. You can argue with me;\nthat's fine. This is just the way <em>I<\/em> do it.\n<img src=\"https:\/\/blog.stanleysolutionsnw.com\/2022-11-02_16-40-03.png\" style=\"width: 100%\" alt=\"Step 4 - Change the Path\"><\/p>\n<h2>5) Install!<\/h2>\n<h2>6) Verify the Path<\/h2>\n<p>It's time to make sure that Python got installed and the Path variable was set correctly.<\/p>\n<p>Press your Windows key and search for \"path\". Then open the \"Edit the system environment variables\" dialog.\n<img src=\"https:\/\/blog.stanleysolutionsnw.com\/2022-11-02_16-37-41.png\" style=\"width: 100%\" alt=\"Step 6 - Verify the Path\"><\/p>\n<p>From the dialog, select \"Environment Variables\" in the bottom-right.<\/p>\n<p>Then, in the bottom window, make sure that you can see \"<code>C:\\Python311\\Scripts\\<\/code>\" and \"<code>C:\\Python311\\<\/code>\" listed in the \"Path\" variable.\nIf they're not there, double click on the \"Path\" variable, and add them!\n<img src=\"https:\/\/blog.stanleysolutionsnw.com\/2022-11-02_16-45-01.png\" style=\"width: 100%\" alt=\"Step 6 - Verify the Path\"><\/p>\n<hr>\n<p>That's about it! Have fun with the faster, newer, Python, everyone!<\/p>","category":[{"@attributes":{"term":"Python"}},{"@attributes":{"term":"python"}},{"@attributes":{"term":"windows"}},{"@attributes":{"term":"development"}},{"@attributes":{"term":"installing"}}]},{"title":"Making Drawing Circuits in Markdown a Cinch!","link":{"@attributes":{"href":"https:\/\/blog.stanleysolutionsnw.com\/making-drawing-circuits-in-markdown-a-cinch.html","rel":"alternate"}},"published":"2022-08-30T13:01:00-07:00","updated":"2022-08-30T13:01:00-07:00","author":{"name":"Joe Stanley"},"id":"tag:blog.stanleysolutionsnw.com,2022-08-30:\/making-drawing-circuits-in-markdown-a-cinch.html","summary":"<p>I've talked about how someone can make PlantUML diagrams come to life, directly in markdown for blog-sites, and I've touched on other automation techniques I use to make blogs come together from plain text, but how about some circuit diagrams? Well, there wasn't a neat tool to help make this a possibility, until now!<\/p>","content":"<p>So, wouldn't it be nice to be able to draw circuits right in your markdown? The same way that PlantUML drawings can be carved out from plain-text.<\/p>\n<blockquote>\n<p><em>Sure, but that technology doesn't exist... right?<\/em><\/p>\n<\/blockquote>\n<p>Hah! That's what you thought...<\/p>\n<p>Enter: <a href=\"https:\/\/github.com\/engineerjoe440\/schemdraw-markdown\"><code>schemdraw-markdown<\/code><\/a>, a brand-new tool built by yours truly that can take\n<a href=\"https:\/\/schemdraw.readthedocs.io\/en\/latest\/index.html\"><code>schemdraw<\/code><\/a> logic embedded in special blocks of markdown and build appropriate SVG circuits to illustrate\nthe circuits, embedding them directly in the markdown!<\/p>\n<p>I can't take all of the credit, there's some good folks who built the <a href=\"https:\/\/github.com\/mikitex70\/plantuml-markdown\"><code>plantuml-markdown<\/code> extension<\/a> extension,\nwhich I've already built into my Pelican-blogsite generation, allowing me to make those awesome little drawings in some of my\n<a href=\"https:\/\/blog.stanleysolutionsnw.com\/making-feline-stink-a-distant-memory.html\">past<\/a>\n<a href=\"https:\/\/blog.stanleysolutionsnw.com\/using-python-to-provide-simple-photo-connections-for-youth.html\">articles<\/a>. That tool was the starting-point for my work.\nCall it inspiration; call it shameless, code-plundering; they built an excellent framework which I was able to reconfigure to support <code>schemdraw<\/code>. Either way,\nthose folks built an awesome tool, and made it really easy for me to build something similar.<\/p>\n<h2>C'mon, let's show this off!<\/h2>\n<p>Want to see it in action? Here's a few samples:<\/p>\n<h4>The Standard Schemdraw Intro Diagram:<\/h4>\n<p><img src=\"data:image\/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB4bWw6bGFuZz0iZW4iIGhlaWdodD0iMTM5LjE2NnB0IiB3aWR0aD0iMTY3Ljk4OHB0IiB2aWV3Qm94PSItNDYuMzg4MDAwMDAwMDAwMDEgLTI2LjU2NiAxNjcuOTg4IDEzOS4xNjYiPjxjaXJjbGUgY3g9Ii0xLjI2ODA2NjUxOTY5MDQ0MDJlLTE0IiBjeT0iNTMuOTk5OTk5OTk5OTk5OTg2IiByPSIxOC4wIiBzdHlsZT0ic3Ryb2tlOndoaXRlO2ZpbGw6bm9uZTtzdHJva2Utd2lkdGg6Mi4wO3N0cm9rZS1kYXNoYXJyYXk6LTsiIC8+PHBhdGggZD0iTSAwLjAsLTAuMCBMIDM2LjAsLTAuMCBMIDM5LjAsLTkuMCBMIDQ1LjAsOS4wIEwgNTAuOTk5OTk5OTk5OTk5OTksLTkuMCBMIDU3LjAsOS4wIEwgNjMuMCwtOS4wIEwgNjkuMCw5LjAgTCA3Mi4wLC0wLjAgTCAxMDguMCwtMC4wIiBzdHlsZT0ic3Ryb2tlOndoaXRlO2ZpbGw6bm9uZTtzdHJva2Utd2lkdGg6Mi4wO3N0cm9rZS1kYXNoYXJyYXk6LTtzdHJva2UtbGluZWNhcDpyb3VuZDtzdHJva2UtbGluZWpvaW46cm91bmQ7IiAvPjxwYXRoIGQ9Ik0gMTA4LjAsLTAuMCBMIDEwNy45OTk5OTk5OTk5OTk5OSw1MC43NiBNIDExNi45OTk5OTk5OTk5OTk5OSw1MC43NiBMIDk4Ljk5OTk5OTk5OTk5OTk5LDUwLjc2IE0gMTE2Ljk5OTk5OTk5OTk5OTk5LDU3LjIzOTk5OTk5OTk5OTk5NSBMIDk4Ljk5OTk5OTk5OTk5OTk5LDU3LjIzOTk5OTk5OTk5OTk5NSBNIDEwNy45OTk5OTk5OTk5OTk5OSw1Ny4yMzk5OTk5OTk5OTk5OTUgTCAxMDcuOTk5OTk5OTk5OTk5OTksMTA4LjAiIHN0eWxlPSJzdHJva2U6d2hpdGU7ZmlsbDpub25lO3N0cm9rZS13aWR0aDoyLjA7c3Ryb2tlLWRhc2hhcnJheTotO3N0cm9rZS1saW5lY2FwOnJvdW5kO3N0cm9rZS1saW5lam9pbjpyb3VuZDsiIC8+PHBhdGggZD0iTSAxMDcuOTk5OTk5OTk5OTk5OTksMTA4LjAgTCA1My45OTk5OTk5OTk5OTk5ODYsMTA4LjAgTCAtMS41OTg3MjExNTU0NjAyMjU0ZS0xNCwxMDcuOTk5OTk5OTk5OTk5OTkiIHN0eWxlPSJzdHJva2U6d2hpdGU7ZmlsbDpub25lO3N0cm9rZS13aWR0aDoyLjA7c3Ryb2tlLWRhc2hhcnJheTotO3N0cm9rZS1saW5lY2FwOnJvdW5kO3N0cm9rZS1saW5lam9pbjpyb3VuZDsiIC8+PHBhdGggZD0iTSAtMS41OTg3MjExNTU0NjAyMjU0ZS0xNCwxMDcuOTk5OTk5OTk5OTk5OTkgTCAtMS4zNzgyODQ3MzE2MTM3MDE4ZS0xNCw3MS45OTk5OTk5OTk5OTk5OSBMIC0xLjM3ODI4NDczMTYxMzcwMThlLTE0LDcxLjk5OTk5OTk5OTk5OTk5IE0gLTEuMTU3ODQ4MzA3NzY3MTc4M2UtMTQsMzUuOTk5OTk5OTk5OTk5OTg2IEwgLTEuMTU3ODQ4MzA3NzY3MTc4M2UtMTQsMzUuOTk5OTk5OTk5OTk5OTg2IEwgLTkuMzc0MTE4ODM5MjA2NTQ3ZS0xNSwtMS41OTg3MjExNTU0NjAyMjU0ZS0xNCIgc3R5bGU9InN0cm9rZTp3aGl0ZTtmaWxsOm5vbmU7c3Ryb2tlLXdpZHRoOjIuMDtzdHJva2UtZGFzaGFycmF5Oi07c3Ryb2tlLWxpbmVjYXA6cm91bmQ7c3Ryb2tlLWxpbmVqb2luOnJvdW5kOyIgLz48cGF0aCBkPSJNIDguOTk5OTk5OTk5OTk5OTg4LDUzLjk5OTk5OTk5OTk5OTk4NiBMIDguMjQ5OTk5OTk5OTk5OTg2LDUyLjEzNjUwMjg3NTI2MTgyNiBMIDcuNDk5OTk5OTk5OTk5OTg4NSw1MC4zOTk5OTk5OTk5OTk5OCBMIDYuNzQ5OTk5OTk5OTk5OTg4LDQ4LjkwODgzMTE3NTQ1Njg0IEwgNS45OTk5OTk5OTk5OTk5ODg1LDQ3Ljc2NDYxNzA5Mjc1MjAyNiBMIDUuMjQ5OTk5OTk5OTk5OTg4NSw0Ny4wNDUzMzQwNTA3MTg2OTQgTCA0LjQ5OTk5OTk5OTk5OTk4OCw0Ni43OTk5OTk5OTk5OTk5OCBMIDMuNzQ5OTk5OTk5OTk5OTg4LDQ3LjA0NTMzNDA1MDcxODY5IEwgMi45OTk5OTk5OTk5OTk5ODgsNDcuNzY0NjE3MDkyNzUyMDI2IEwgMi4yNDk5OTk5OTk5OTk5ODc2LDQ4LjkwODgzMTE3NTQ1Njg1IEwgMS40OTk5OTk5OTk5OTk5ODgyLDUwLjM5OTk5OTk5OTk5OTk4IEwgMC43NDk5OTk5OTk5OTk5ODc3LDUyLjEzNjUwMjg3NTI2MTgzIEwgLTEuMjY4MDY2NTE5NjkwNDQwMmUtMTQsNTMuOTk5OTk5OTk5OTk5OTg2IEwgLTAuNzUwMDAwMDAwMDAwMDEyMSw1NS44NjM0OTcxMjQ3MzgxMyBMIC0xLjUwMDAwMDAwMDAwMDAxMTUsNTcuNTk5OTk5OTk5OTk5OTggTCAtMi4yNTAwMDAwMDAwMDAwMTMsNTkuMDkxMTY4ODI0NTQzMTMgTCAtMy4wMDAwMDAwMDAwMDAwMTI0LDYwLjIzNTM4MjkwNzI0Nzk0IEwgLTMuNzUwMDAwMDAwMDAwMDExNSw2MC45NTQ2NjU5NDkyODEyNyBMIC00LjUwMDAwMDAwMDAwMDAxMyw2MS4xOTk5OTk5OTk5OTk5OCBMIC01LjI1MDAwMDAwMDAwMDAxMiw2MC45NTQ2NjU5NDkyODEyOCBMIC02LjAwMDAwMDAwMDAwMDAxMTUsNjAuMjM1MzgyOTA3MjQ3OTQgTCAtNi43NTAwMDAwMDAwMDAwMTMsNTkuMDkxMTY4ODI0NTQzMTI1IEwgLTcuNTAwMDAwMDAwMDAwMDEyLDU3LjU5OTk5OTk5OTk5OTk5IEwgLTguMjUwMDAwMDAwMDAwMDEyLDU1Ljg2MzQ5NzEyNDczODEzIEwgLTkuMDAwMDAwMDAwMDAwMDEyLDUzLjk5OTk5OTk5OTk5OTk4NiIgc3R5bGU9InN0cm9rZTp3aGl0ZTtmaWxsOm5vbmU7c3Ryb2tlLXdpZHRoOjIuMDtzdHJva2UtZGFzaGFycmF5Oi07c3Ryb2tlLWxpbmVjYXA6cm91bmQ7c3Ryb2tlLWxpbmVqb2luOnJvdW5kOyIgLz48Zz48Zz48Zz48c3ltYm9sIGlkPSJTVElYVHdvTWF0aFJlZ3VsYXJfNDI0NCIgdmlld0JveD0iMCAtMzEuNTI0IDMuNzU2IDUxLjIxNiI+PHBhdGggZD0iTSAzLjc1NiAwIEwgMy43NTYgMCBMIDIuNzYgMCBMIDIuNzYgLTYuMzQ4IEwgMS4yMTIgLTYuMzQ4IEwgMS4yMTIgLTYuOTk2IFEgMS44NDggLTcuMDkyIDIuMjU2IC03LjIzIFEgMi42NjQgLTcuMzY4IDMgLTcuNTYgTCAzLjc1NiAtNy41NiBaICIgLz48L3N5bWJvbD48dXNlIGhyZWY9IiNTVElYVHdvTWF0aFJlZ3VsYXJfNDI0NCIgeD0iNDUuNzEyIiB5PSItNDkuMzc4IiB3aWR0aD0iNC4zODIiIGhlaWdodD0iNTkuNzUyIiBmaWxsPSJ3aGl0ZSIgLz48c3ltYm9sIGlkPSJTVElYVHdvTWF0aFJlZ3VsYXJfODY4IiB2aWV3Qm94PSIwIC0zMS41MjQgOC43NzIgNTEuMjE2Ij48cGF0aCBkPSJNIDguNDk2IDAgTCA4LjQ5NiAwIEwgNS4zNjQgMCBMIDUuMzY0IC0wLjg2NCBRIDUuOTI4IC0xLjMyIDYuMzE4IC0xLjk1IFEgNi43MDggLTIuNTggNi45MDYgLTMuMjgyIFEgNy4xMDQgLTMuOTg0IDcuMTA0IC00LjY0NCBRIDcuMTA0IC02IDYuNDU2IC02Ljc2MiBRIDUuODA4IC03LjUyNCA0LjcwNCAtNy41MjQgUSAzLjU2NCAtNy41MjQgMi45MDQgLTYuNzUgUSAyLjI0NCAtNS45NzYgMi4yNDQgLTQuNjQ0IFEgMi4yNDQgLTMuOTk2IDIuNDM2IC0zLjI4MiBRIDIuNjI4IC0yLjU2OCAzLjAxOCAtMS45MzIgUSAzLjQwOCAtMS4yOTYgMy45ODQgLTAuODUyIEwgMy45ODQgMCBMIDAuODE2IDAgTCAwLjU2NCAtMi4xMjQgTCAwLjkxMiAtMi4xMjQgUSAxLjAyIC0xLjcxNiAxLjEyMiAtMS40NjQgUSAxLjIyNCAtMS4yMTIgMS40MSAtMS4wOTIgUSAxLjU5NiAtMC45NzIgMS45MzIgLTAuOTcyIEwgMy4wMjQgLTAuOTcyIEwgMy4wMjQgLTEuMDQ0IFEgMi42ODggLTEuMjk2IDIuMzIyIC0xLjYyNiBRIDEuOTU2IC0xLjk1NiAxLjY0NCAtMi4zODggUSAxLjMzMiAtMi44MiAxLjE0IC0zLjM5IFEgMC45NDggLTMuOTYgMC45NDggLTQuNjkyIFEgMC45NDggLTUuNjg4IDEuNDI4IC02LjQzOCBRIDEuOTA4IC03LjE4OCAyLjc2IC03LjYwOCBRIDMuNjEyIC04LjAyOCA0LjcxNiAtOC4wMjggUSA1LjgzMiAtOC4wMjggNi42NiAtNy42MDggUSA3LjQ4OCAtNy4xODggNy45NDQgLTYuNDM4IFEgOC40IC01LjY4OCA4LjQgLTQuNzA0IFEgOC40IC0zLjk3MiA4LjE5NiAtMy40MDIgUSA3Ljk5MiAtMi44MzIgNy42NzQgLTIuMzk0IFEgNy4zNTYgLTEuOTU2IDcuMDAyIC0xLjYyNiBRIDYuNjQ4IC0xLjI5NiA2LjMzNiAtMS4wNDQgTCA2LjMzNiAtMC45NzIgTCA3LjM0NCAtMC45NzIgUSA3LjcwNCAtMC45NzIgNy44OSAtMS4wOTIgUSA4LjA3NiAtMS4yMTIgOC4xOSAtMS40NyBRIDguMzA0IC0xLjcyOCA4LjQyNCAtMi4xMjQgTCA4Ljc3MiAtMi4xMjQgWiAiIC8+PC9zeW1ib2w+PHVzZSBocmVmPSIjU1RJWFR3b01hdGhSZWd1bGFyXzg2OCIgeD0iNTIuODI0IiB5PSItNDkuMzc4IiB3aWR0aD0iMTAuMjM0IiBoZWlnaHQ9IjU5Ljc1MiIgZmlsbD0id2hpdGUiIC8+PC9nPjwvZz48L2c+PGc+PGc+PGc+PHN5bWJvbCBpZD0iU1RJWFR3b01hdGhSZWd1bGFyXzQyNDQiIHZpZXdCb3g9IjAgLTMxLjUyNCAzLjc1NiA1MS4yMTYiPjxwYXRoIGQ9Ik0gMy43NTYgMCBMIDMuNzU2IDAgTCAyLjc2IDAgTCAyLjc2IC02LjM0OCBMIDEuMjEyIC02LjM0OCBMIDEuMjEyIC02Ljk5NiBRIDEuODQ4IC03LjA5MiAyLjI1NiAtNy4yMyBRIDIuNjY0IC03LjM2OCAzIC03LjU2IEwgMy43NTYgLTcuNTYgWiAiIC8+PC9zeW1ib2w+PHVzZSBocmVmPSIjU1RJWFR3b01hdGhSZWd1bGFyXzQyNDQiIHg9IjY3Ljc3OCIgeT0iMjAuMzAyIiB3aWR0aD0iNC4zODIiIGhlaWdodD0iNTkuNzUyIiBmaWxsPSJ3aGl0ZSIgLz48c3ltYm9sIGlkPSJTVElYVHdvTWF0aFJlZ3VsYXJfNDI0MyIgdmlld0JveD0iMCAtMzEuNTI0IDUuNTU2IDUxLjIxNiI+PHBhdGggZD0iTSAzLjA0OCAwLjE0NCBMIDMuMDQ4IDAuMTQ0IFEgMS44ODQgMC4xNDQgMS4yMTggLTAuODgyIFEgMC41NTIgLTEuOTA4IDAuNTUyIC0zLjgwNCBRIDAuNTUyIC01LjcxMiAxLjIxOCAtNi43MDIgUSAxLjg4NCAtNy42OTIgMy4wNDggLTcuNjkyIFEgNC4yMjQgLTcuNjkyIDQuODkgLTYuNjk2IFEgNS41NTYgLTUuNyA1LjU1NiAtMy44MDQgUSA1LjU1NiAtMS45MDggNC44OSAtMC44ODIgUSA0LjIyNCAwLjE0NCAzLjA0OCAwLjE0NCBaIE0gMy4wNDggLTAuNjcyIEwgMy4wNDggLTAuNjcyIFEgMy43MzIgLTAuNjcyIDQuMTUyIC0xLjQxIFEgNC41NzIgLTIuMTQ4IDQuNTcyIC0zLjgwNCBRIDQuNTcyIC01LjQ2IDQuMTUyIC02LjE3NCBRIDMuNzMyIC02Ljg4OCAzLjA0OCAtNi44ODggUSAyLjM3NiAtNi44ODggMS45NSAtNi4xNzQgUSAxLjUyNCAtNS40NiAxLjUyNCAtMy44MDQgUSAxLjUyNCAtMi4xNDggMS45NSAtMS40MSBRIDIuMzc2IC0wLjY3MiAzLjA0OCAtMC42NzIgWiAiIC8+PC9zeW1ib2w+PHVzZSBocmVmPSIjU1RJWFR3b01hdGhSZWd1bGFyXzQyNDMiIHg9Ijc0Ljg5IiB5PSIyMC4zMDIiIHdpZHRoPSI2LjQ4MiIgaGVpZ2h0PSI1OS43NTIiIGZpbGw9IndoaXRlIiAvPjxzeW1ib2wgaWQ9IlNUSVhUd29NYXRoUmVndWxhcl84OTYiIHZpZXdCb3g9IjAgLTMxLjUyNCA2LjY4NCA1MS4yMTYiPjxwYXRoIGQ9Ik0gNS40NzIgLTUuNTY4IEwgNS40NzIgLTUuNTY4IEwgNC45NTYgLTIuNjA0IFEgNC45NTYgLTIuNTU2IDQuOTY4IC0yLjM3NiBRIDQuOTggLTIuMTk2IDQuOTkyIC0yLjA2NCBRIDUuMDQgLTEuMzggNS4yMiAtMS4wNzQgUSA1LjQgLTAuNzY4IDUuNzM2IC0wLjc2OCBRIDYuMDI0IC0wLjc2OCA2LjE4NiAtMC45MTIgUSA2LjM0OCAtMS4wNTYgNi40OCAtMS4yMzYgTCA2LjY4NCAtMS4xMDQgUSA2LjU4OCAtMC44MjggNi40MTQgLTAuNTQgUSA2LjI0IC0wLjI1MiA1Ljk3IC0wLjA2NiBRIDUuNyAwLjEyIDUuMzA0IDAuMTIgUSA0Ljg2IDAuMTIgNC42OTggLTAuMTYyIFEgNC41MzYgLTAuNDQ0IDQuNDY0IC0wLjg2NCBRIDQuNDQgLTEuMDQ0IDQuNDEgLTEuMjY2IFEgNC4zOCAtMS40ODggNC4zMzIgLTEuOTY4IEwgNC4yNiAtMS45NjggUSA0LjA2OCAtMS4wOTIgMy43OTggLTAuNjMgUSAzLjUyOCAtMC4xNjggMy4yMjIgLTAuMDA2IFEgMi45MTYgMC4xNTYgMi42MTYgMC4xNTYgUSAyLjI5MiAwLjE1NiAyLjA3IC0wLjAxOCBRIDEuODQ4IC0wLjE5MiAxLjcxIC0wLjQyNiBRIDEuNTcyIC0wLjY2IDEuNDg4IC0wLjgyOCBMIDEuNDI4IC0wLjgyOCBRIDEuNDQgLTAuMzQ4IDEuNDUyIDAuMTAyIFEgMS40NjQgMC41NTIgMS41MDYgMS4wNjggUSAxLjU0OCAxLjU4NCAxLjYyIDIuMjkyIEwgMC43MiAyLjYwNCBMIDAuNTI4IDIuNTY4IFEgMC41NCAxLjcyOCAwLjU3IDEuMDM4IFEgMC42IDAuMzQ4IDAuNjc4IC0wLjMyNCBRIDAuNzU2IC0wLjk5NiAwLjkgLTEuNzg4IEwgMC44ODggLTUuNjQgTCAxLjk2OCAtNS42NzYgTCAyLjAxNiAtNS41OCBMIDEuNTg0IC0yLjYwNCBRIDEuNTg0IC0yLjYwNCAxLjU3OCAtMi41MjYgUSAxLjU3MiAtMi40NDggMS41NzIgLTIuMzQgUSAxLjU3MiAtMS45OCAxLjcxIC0xLjYzOCBRIDEuODQ4IC0xLjI5NiAyLjA5NCAtMS4wNjggUSAyLjM0IC0wLjg0IDIuNjY0IC0wLjg0IFEgMi45ODggLTAuODQgMy4yNTIgLTEuMDY4IFEgMy41MTYgLTEuMjk2IDMuNzIgLTEuNjQ0IFEgMy45MjQgLTEuOTkyIDQuMDYyIC0yLjM1MiBRIDQuMiAtMi43MTIgNC4yNzIgLTMgUSA0LjM0NCAtMy4yODggNC4zNDQgLTMuMzcyIEwgNC4zOCAtNS42NCBMIDUuNDEyIC01LjY3NiBaICIgLz48L3N5bWJvbD48dXNlIGhyZWY9IiNTVElYVHdvTWF0aFJlZ3VsYXJfODk2IiB4PSI4Mi4wMDIiIHk9IjIwLjMwMiIgd2lkdGg9IjcuNzk4IiBoZWlnaHQ9IjU5Ljc1MiIgZmlsbD0id2hpdGUiIC8+PHN5bWJvbCBpZD0iU1RJWFR3b01hdGhSZWd1bGFyXzM2NjQiIHZpZXdCb3g9IjAgLTMxLjUyNCA1Ljc0OCA1MS4yMTYiPjxwYXRoIGQ9Ik0gMS4xMDQgMCBMIDEuMTA0IDAgTCAxLjEwNCAtNy44ODQgTCA1Ljc0OCAtNy44ODQgTCA1Ljc0OCAtNy4wMiBMIDIuMTM2IC03LjAyIEwgMi4xMzYgLTQuMzggTCA1LjE5NiAtNC4zOCBMIDUuMTk2IC0zLjUxNiBMIDIuMTM2IC0zLjUxNiBMIDIuMTM2IDAgWiAiIC8+PC9zeW1ib2w+PHVzZSBocmVmPSIjU1RJWFR3b01hdGhSZWd1bGFyXzM2NjQiIHg9Ijg5LjczIiB5PSIyMC4zMDIiIHdpZHRoPSI2LjcwNiIgaGVpZ2h0PSI1OS43NTIiIGZpbGw9IndoaXRlIiAvPjwvZz48L2c+PC9nPjxnPjxnPjxnPjxzeW1ib2wgaWQ9IlNUSVhUd29NYXRoUmVndWxhcl80MjQ0IiB2aWV3Qm94PSIwIC0zMS41MjQgMy43NTYgNTEuMjE2Ij48cGF0aCBkPSJNIDMuNzU2IDAgTCAzLjc1NiAwIEwgMi43NiAwIEwgMi43NiAtNi4zNDggTCAxLjIxMiAtNi4zNDggTCAxLjIxMiAtNi45OTYgUSAxLjg0OCAtNy4wOTIgMi4yNTYgLTcuMjMgUSAyLjY2NCAtNy4zNjggMyAtNy41NiBMIDMuNzU2IC03LjU2IFogIiAvPjwvc3ltYm9sPjx1c2UgaHJlZj0iI1NUSVhUd29NYXRoUmVndWxhcl80MjQ0IiB4PSItNDEuNzg4IiB5PSIyMS43MzciIHdpZHRoPSI0LjM4MiIgaGVpZ2h0PSI1OS43NTIiIGZpbGw9IndoaXRlIiAvPjxzeW1ib2wgaWQ9IlNUSVhUd29NYXRoUmVndWxhcl80MjQzIiB2aWV3Qm94PSIwIC0zMS41MjQgNS41NTYgNTEuMjE2Ij48cGF0aCBkPSJNIDMuMDQ4IDAuMTQ0IEwgMy4wNDggMC4xNDQgUSAxLjg4NCAwLjE0NCAxLjIxOCAtMC44ODIgUSAwLjU1MiAtMS45MDggMC41NTIgLTMuODA0IFEgMC41NTIgLTUuNzEyIDEuMjE4IC02LjcwMiBRIDEuODg0IC03LjY5MiAzLjA0OCAtNy42OTIgUSA0LjIyNCAtNy42OTIgNC44OSAtNi42OTYgUSA1LjU1NiAtNS43IDUuNTU2IC0zLjgwNCBRIDUuNTU2IC0xLjkwOCA0Ljg5IC0wLjg4MiBRIDQuMjI0IDAuMTQ0IDMuMDQ4IDAuMTQ0IFogTSAzLjA0OCAtMC42NzIgTCAzLjA0OCAtMC42NzIgUSAzLjczMiAtMC42NzIgNC4xNTIgLTEuNDEgUSA0LjU3MiAtMi4xNDggNC41NzIgLTMuODA0IFEgNC41NzIgLTUuNDYgNC4xNTIgLTYuMTc0IFEgMy43MzIgLTYuODg4IDMuMDQ4IC02Ljg4OCBRIDIuMzc2IC02Ljg4OCAxLjk1IC02LjE3NCBRIDEuNTI0IC01LjQ2IDEuNTI0IC0zLjgwNCBRIDEuNTI0IC0yLjE0OCAxLjk1IC0xLjQxIFEgMi4zNzYgLTAuNjcyIDMuMDQ4IC0wLjY3MiBaICIgLz48L3N5bWJvbD48dXNlIGhyZWY9IiNTVElYVHdvTWF0aFJlZ3VsYXJfNDI0MyIgeD0iLTM0LjY3NiIgeT0iMjEuNzM3IiB3aWR0aD0iNi40ODIiIGhlaWdodD0iNTkuNzUyIiBmaWxsPSJ3aGl0ZSIgLz48c3ltYm9sIGlkPSJTVElYVHdvTWF0aFJlZ3VsYXJfMzY4MCIgdmlld0JveD0iMCAtMzEuNTI0IDYuMzI0IDUxLjIxNiI+PHBhdGggZD0iTSAyLjU4IDAgTCAyLjU4IDAgTCAwLjAxMiAtNy44ODQgTCAxLjEwNCAtNy44ODQgTCAyLjQgLTMuNjM2IFEgMi42MDQgLTIuOTQgMi43NzIgLTIuMzM0IFEgMi45NCAtMS43MjggMy4xNjggLTEuMDMyIEwgMy4yMjggLTEuMDMyIFEgMy40NDQgLTEuNzI4IDMuNjE4IC0yLjMzNCBRIDMuNzkyIC0yLjk0IDMuOTk2IC0zLjYzNiBMIDUuMjggLTcuODg0IEwgNi4zMjQgLTcuODg0IEwgMy43NjggMCBaICIgLz48L3N5bWJvbD48dXNlIGhyZWY9IiNTVElYVHdvTWF0aFJlZ3VsYXJfMzY4MCIgeD0iLTI3LjU2NCIgeT0iMjEuNzM3IiB3aWR0aD0iNy4zNzgiIGhlaWdodD0iNTkuNzUyIiBmaWxsPSJ3aGl0ZSIgLz48L2c+PC9nPjwvZz48L3N2Zz4=\" alt=\"Schemdraw Basic Diagram\" title=\"\" \/><\/p>\n<details>\n  <summary>Click to expand all the Schemdraw-Markdown Goodness!<\/summary>\n\n<div class=\"highlight\"><pre><span><\/span><code>::_schemdraw_:: alt=&quot;My super diagram&quot; color=&quot;white&quot;\n    += elm.Resistor().right().label(&#39;1\u03a9&#39;)\n    += elm.Capacitor().down().label(&#39;10\u03bcF&#39;)\n    += elm.Line().left()\n    += elm.SourceSin().up().label(&#39;10V&#39;)\n::end-schemdraw::\n<\/code><\/pre><\/div>\n\n\n<\/details>\n\n<h4>Something a bit Juicier:<\/h4>\n<p>Example <a href=\"https:\/\/schemdraw.readthedocs.io\/en\/latest\/gallery\/analog.html#discharging-capacitor\">from Schemdraw Docs<\/a><\/p>\n<p><img src=\"data:image\/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB4bWw6bGFuZz0iZW4iIGhlaWdodD0iMTM2Ljk2MjAwMDAwMDAwMDAycHQiIHdpZHRoPSIyNTguOTcwMnB0IiB2aWV3Qm94PSItNDAuMzM5OTk5OTk5OTk5OTkgLTEyOS42NjIgMjU4Ljk3MDIgMTM2Ljk2MjAwMDAwMDAwMDAyIj48Y2lyY2xlIGN4PSIzLjMwNjU0NjM1NzY5Nzg1MzNlLTE1IiBjeT0iLTU0LjAiIHI9IjE4LjAiIHN0eWxlPSJzdHJva2U6d2hpdGU7ZmlsbDpub25lO3N0cm9rZS13aWR0aDoyLjA7c3Ryb2tlLWRhc2hhcnJheTotOyIgLz48cGF0aCBkPSJNIDgzLjU1IC05MS40MyBhIDkuMCAxMy41IC05MCAwIDEgMTMuNDMwMDAwMDAwMDAwMDA3IC00LjYyOTk5OTk5OTk5OTk5NTUiIHN0eWxlPSJzdHJva2U6d2hpdGU7ZmlsbDpub25lO3N0cm9rZS13aWR0aDoyLjA7c3Ryb2tlLWRhc2hhcnJheTotOyIgLz48cGF0aCBkPSJNIDEwMy45MjgyMjIxNjQxNTU5MyAtOTUuMjQxMjIyNDcxMjIxMzMgTCA5Ni42NjA4NzI1ODM3NjMxOCAtOTMuMzc2OTE4MjkxMDA2NzYgTCA5Ny4yOTEzMDg4NzU2NDQ3NyAtOTguNzM5OTkxMTExODY5NjggWiIgc3R5bGU9InN0cm9rZTp3aGl0ZTtmaWxsOndoaXRlO3N0cm9rZS1saW5lY2FwOmJ1dHQ7c3Ryb2tlLWxpbmVqb2luOm1pdGVyOyIgLz48cGF0aCBkPSJNIDAuMCwtMC4wIEwgMi4yMDQzNjQyMzg0NjUyMzZlLTE1LC0zNi4wIEwgMi4yMDQzNjQyMzg0NjUyMzZlLTE1LC0zNi4wIE0gNC40MDg3Mjg0NzY5MzA0NzJlLTE1LC03Mi4wIEwgNC40MDg3Mjg0NzY5MzA0NzJlLTE1LC03Mi4wIEwgNi42MTMwOTI3MTUzOTU3MDdlLTE1LC0xMDguMCIgc3R5bGU9InN0cm9rZTp3aGl0ZTtmaWxsOm5vbmU7c3Ryb2tlLXdpZHRoOjIuMDtzdHJva2UtZGFzaGFycmF5Oi07c3Ryb2tlLWxpbmVjYXA6cm91bmQ7c3Ryb2tlLWxpbmVqb2luOnJvdW5kOyIgLz48cGF0aCBkPSJNIDMuNjAwMDAwMDAwMDAwMDAzLC00NS4wIEwgLTMuNTk5OTk5OTk5OTk5OTk3LC00NS4wIiBzdHlsZT0ic3Ryb2tlOndoaXRlO2ZpbGw6bm9uZTtzdHJva2Utd2lkdGg6Mi4wO3N0cm9rZS1kYXNoYXJyYXk6LTtzdHJva2UtbGluZWNhcDpyb3VuZDtzdHJva2UtbGluZWpvaW46cm91bmQ7IiAvPjxwYXRoIGQ9Ik0gMy42MzcyMDA5OTM0Njc2MzllLTE1LC01OS40IEwgNC4wNzgwNzM4NDExNjA2ODZlLTE1LC02Ni42MDAwMDAwMDAwMDAwMSIgc3R5bGU9InN0cm9rZTp3aGl0ZTtmaWxsOm5vbmU7c3Ryb2tlLXdpZHRoOjIuMDtzdHJva2UtZGFzaGFycmF5Oi07c3Ryb2tlLWxpbmVjYXA6cm91bmQ7c3Ryb2tlLWxpbmVqb2luOnJvdW5kOyIgLz48cGF0aCBkPSJNIDMuNjAwMDAwMDAwMDAwMDA0LC02My4wIEwgLTMuNTk5OTk5OTk5OTk5OTk2LC02My4wIiBzdHlsZT0ic3Ryb2tlOndoaXRlO2ZpbGw6bm9uZTtzdHJva2Utd2lkdGg6Mi4wO3N0cm9rZS1kYXNoYXJyYXk6LTtzdHJva2UtbGluZWNhcDpyb3VuZDtzdHJva2UtbGluZWpvaW46cm91bmQ7IiAvPjxwYXRoIGQ9Ik0gNi42MTMwOTI3MTUzOTU3MDdlLTE1LC0xMDguMCBMIDQwLjUwMDAwMDAwMDAwMDAxLC0xMDguMCBMIDgxLjAsLTEwOC4wIiBzdHlsZT0ic3Ryb2tlOndoaXRlO2ZpbGw6bm9uZTtzdHJva2Utd2lkdGg6Mi4wO3N0cm9rZS1kYXNoYXJyYXk6LTtzdHJva2UtbGluZWNhcDpyb3VuZDtzdHJva2UtbGluZWpvaW46cm91bmQ7IiAvPjxwYXRoIGQ9Ik0gOTUuMzk5OTk5OTk5OTk5OTksLTc2LjMyMDAwMDAwMDAwMDAxIE0gOTEuOCwtODQuOTYgTCA4Ni4zOTk5OTk5OTk5OTk5OSwtMTAxLjUyIE0gODEuMCwtMTEyLjMyMDAwMDAwMDAwMDAxIiBzdHlsZT0ic3Ryb2tlOndoaXRlO2ZpbGw6bm9uZTtzdHJva2Utd2lkdGg6Mi4wO3N0cm9rZS1kYXNoYXJyYXk6LTtzdHJva2UtbGluZWNhcDpyb3VuZDtzdHJva2UtbGluZWpvaW46cm91bmQ7IiAvPjxwYXRoIGQ9Ik0gMTA5LjgsLTEwOC4wIEwgMTUwLjI5OTk5OTk5OTk5OTk4LC0xMDguMCBMIDE5MC43OTk5OTk5OTk5OTk5OCwtMTA4LjAiIHN0eWxlPSJzdHJva2U6d2hpdGU7ZmlsbDpub25lO3N0cm9rZS13aWR0aDoyLjA7c3Ryb2tlLWRhc2hhcnJheTotO3N0cm9rZS1saW5lY2FwOnJvdW5kO3N0cm9rZS1saW5lam9pbjpyb3VuZDsiIC8+PHBhdGggZD0iTSAxOTAuNzk5OTk5OTk5OTk5OTgsLTEwOC4wIEwgMTkwLjc5OTk5OTk5OTk5OTk4LC03Mi4wIEwgMTk5Ljc5OTk5OTk5OTk5OTk4LC02OS4wIEwgMTgxLjc5OTk5OTk5OTk5OTk4LC02My4wIEwgMTk5Ljc5OTk5OTk5OTk5OTk4LC01Ny4wMDAwMDAwMDAwMDAwMSBMIDE4MS43OTk5OTk5OTk5OTk5OCwtNTEuMCBMIDE5OS43OTk5OTk5OTk5OTk5OCwtNDUuMCBMIDE4MS43OTk5OTk5OTk5OTk5OCwtMzkuMDAwMDAwMDAwMDAwMDEgTCAxOTAuNzk5OTk5OTk5OTk5OTgsLTM2LjAgTCAxOTAuNzk5OTk5OTk5OTk5OTUsLTAuMCIgc3R5bGU9InN0cm9rZTp3aGl0ZTtmaWxsOm5vbmU7c3Ryb2tlLXdpZHRoOjIuMDtzdHJva2UtZGFzaGFycmF5Oi07c3Ryb2tlLWxpbmVjYXA6cm91bmQ7c3Ryb2tlLWxpbmVqb2luOnJvdW5kOyIgLz48cGF0aCBkPSJNIDE5MC43OTk5OTk5OTk5OTk5NSwtMC4wIEwgOTUuMzk5OTk5OTk5OTk5OTgsLTEuMTY4MzEzMDQ2Mzg2NTc0OGUtMTQgTCAwLjAsLTIuMzM2NjI2MDkyNzczMTQ5N2UtMTQiIHN0eWxlPSJzdHJva2U6d2hpdGU7ZmlsbDpub25lO3N0cm9rZS13aWR0aDoyLjA7c3Ryb2tlLWRhc2hhcnJheTotO3N0cm9rZS1saW5lY2FwOnJvdW5kO3N0cm9rZS1saW5lam9pbjpyb3VuZDsiIC8+PHBhdGggZD0iTSA5NS4zOTk5OTk5OTk5OTk5OSwtODAuNjQwMDAwMDAwMDAwMDEgTCA5NS4zOTk5OTk5OTk5OTk5OSwtNDMuNTYwMDAwMDAwMDAwMDEgTSAxMDQuMzk5OTk5OTk5OTk5OTksLTQzLjU2MDAwMDAwMDAwMDAxIEwgODYuMzk5OTk5OTk5OTk5OTksLTQzLjU2MDAwMDAwMDAwMDAxIE0gMTA0LjM5OTk5OTk5OTk5OTk5LC0zNy4wODAwMDAwMDAwMDAwMSBMIDg2LjM5OTk5OTk5OTk5OTk5LC0zNy4wODAwMDAwMDAwMDAwMSBNIDk1LjM5OTk5OTk5OTk5OTk5LC0zNy4wODAwMDAwMDAwMDAwMSBMIDk1LjM5OTk5OTk5OTk5OTk5LC0wLjAiIHN0eWxlPSJzdHJva2U6d2hpdGU7ZmlsbDpub25lO3N0cm9rZS13aWR0aDoyLjA7c3Ryb2tlLWRhc2hhcnJheTotO3N0cm9rZS1saW5lY2FwOnJvdW5kO3N0cm9rZS1saW5lam9pbjpyb3VuZDsiIC8+PGc+PGc+PGc+PHN5bWJvbCBpZD0iU1RJWFR3b01hdGhSZWd1bGFyXzQyNDgiIHZpZXdCb3g9IjAgLTMxLjUyNCA1LjUwOCA1MS4yMTYiPjxwYXRoIGQ9Ik0gMi44NjggMC4xNDQgTCAyLjg2OCAwLjE0NCBRIDEuOTMyIDAuMTQ0IDEuMzIgLTAuMTggUSAwLjcwOCAtMC41MDQgMC4zIC0wLjg4OCBMIDAuODA0IC0xLjU0OCBRIDEuMTUyIC0xLjIxMiAxLjYxNCAtMC45NTQgUSAyLjA3NiAtMC42OTYgMi43NzIgLTAuNjk2IFEgMy40OCAtMC42OTYgMy45NzggLTEuMTU4IFEgNC40NzYgLTEuNjIgNC40NzYgLTIuNCBRIDQuNDc2IC0zLjE4IDQuMDI2IC0zLjYwNiBRIDMuNTc2IC00LjAzMiAyLjgzMiAtNC4wMzIgUSAyLjQyNCAtNC4wMzIgMi4xMyAtMy45MTggUSAxLjgzNiAtMy44MDQgMS40ODggLTMuNTc2IEwgMC45MzYgLTMuOTI0IEwgMS4yIC03LjU2IEwgNS4xMTIgLTcuNTYgTCA1LjExMiAtNi42ODQgTCAyLjA4OCAtNi42ODQgTCAxLjg4NCAtNC40ODggUSAyLjE2IC00LjYzMiAyLjQzNiAtNC43MTYgUSAyLjcxMiAtNC44IDMuMDg0IC00LjggUSAzLjc1NiAtNC44IDQuMzAyIC00LjU0OCBRIDQuODQ4IC00LjI5NiA1LjE3OCAtMy43NzQgUSA1LjUwOCAtMy4yNTIgNS41MDggLTIuNDI0IFEgNS41MDggLTEuNjA4IDUuMTM2IC0xLjAzMiBRIDQuNzY0IC0wLjQ1NiA0LjE1OCAtMC4xNTYgUSAzLjU1MiAwLjE0NCAyLjg2OCAwLjE0NCBaICIgLz48L3N5bWJvbD48dXNlIGhyZWY9IiNTVElYVHdvTWF0aFJlZ3VsYXJfNDI0OCIgeD0iLTM1Ljc0IiB5PSItODYuMjYzIiB3aWR0aD0iNi40MjYiIGhlaWdodD0iNTkuNzUyIiBmaWxsPSJ3aGl0ZSIgLz48c3ltYm9sIGlkPSJTVElYVHdvTWF0aFJlZ3VsYXJfMzY4MCIgdmlld0JveD0iMCAtMzEuNTI0IDYuMzI0IDUxLjIxNiI+PHBhdGggZD0iTSAyLjU4IDAgTCAyLjU4IDAgTCAwLjAxMiAtNy44ODQgTCAxLjEwNCAtNy44ODQgTCAyLjQgLTMuNjM2IFEgMi42MDQgLTIuOTQgMi43NzIgLTIuMzM0IFEgMi45NCAtMS43MjggMy4xNjggLTEuMDMyIEwgMy4yMjggLTEuMDMyIFEgMy40NDQgLTEuNzI4IDMuNjE4IC0yLjMzNCBRIDMuNzkyIC0yLjk0IDMuOTk2IC0zLjYzNiBMIDUuMjggLTcuODg0IEwgNi4zMjQgLTcuODg0IEwgMy43NjggMCBaICIgLz48L3N5bWJvbD48dXNlIGhyZWY9IiNTVElYVHdvTWF0aFJlZ3VsYXJfMzY4MCIgeD0iLTI4LjYyOCIgeT0iLTg2LjI2MyIgd2lkdGg9IjcuMzc4IiBoZWlnaHQ9IjU5Ljc1MiIgZmlsbD0id2hpdGUiIC8+PC9nPjwvZz48L2c+PGNpcmNsZSBjeD0iOTUuMzk5OTk5OTk5OTk5OTkiIGN5PSItODAuNjQwMDAwMDAwMDAwMDEiIHI9IjQuMzIiIHN0eWxlPSJzdHJva2U6d2hpdGU7ZmlsbDp3aGl0ZTtzdHJva2Utd2lkdGg6Mi4wO3N0cm9rZS1kYXNoYXJyYXk6LTsiIC8+PGNpcmNsZSBjeD0iMTA5LjgiIGN5PSItMTA4LjAiIHI9IjQuMzIiIHN0eWxlPSJzdHJva2U6d2hpdGU7ZmlsbDp3aGl0ZTtzdHJva2Utd2lkdGg6Mi4wO3N0cm9rZS1kYXNoYXJyYXk6LTsiIC8+PGNpcmNsZSBjeD0iODEuMCIgY3k9Ii0xMDguMCIgcj0iNC4zMiIgc3R5bGU9InN0cm9rZTp3aGl0ZTtmaWxsOndoaXRlO3N0cm9rZS13aWR0aDoyLjA7c3Ryb2tlLWRhc2hhcnJheTotOyIgLz48Zz48Zz48Zz48c3ltYm9sIGlkPSJTVElYVHdvTWF0aFJlZ3VsYXJfMzM0NyIgdmlld0JveD0iMCAtMzEuNTI0IDMuODA0IDUxLjIxNiI+PHBhdGggZD0iTSAzLjgwNCAtNS42MDQgTCAzLjgwNCAtNS42MDQgTCAzLjcwOCAtNS4xMzYgTCAyLjQ5NiAtNS4xMzYgTCAxLjc4OCAtMS41NiBRIDEuNzg4IC0xLjU2IDEuNzQ2IC0xLjMzOCBRIDEuNzA0IC0xLjExNiAxLjcwNCAtMC45NiBRIDEuNzA0IC0wLjg2NCAxLjc0NiAtMC43ODYgUSAxLjc4OCAtMC43MDggMS45MDggLTAuNzA4IFEgMi4xNzIgLTAuNzA4IDIuMzk0IC0wLjg2NCBRIDIuNjE2IC0xLjAyIDIuOTI4IC0xLjQyOCBMIDMuMTY4IC0xLjI0OCBRIDMgLTEuMDIgMi43NiAtMC42OTYgUSAyLjUyIC0wLjM3MiAyLjE4NCAtMC4xMzIgUSAxLjg0OCAwLjEwOCAxLjM2OCAwLjEwOCBRIDAuOTk2IDAuMTA4IDAuODE2IC0wLjA3MiBRIDAuNjM2IC0wLjI1MiAwLjYzNiAtMC41MjggUSAwLjYzNiAtMC42ODQgMC42NzIgLTAuOTA2IFEgMC43MDggLTEuMTI4IDAuNzIgLTEuMjEyIEwgMS41MzYgLTUuMTM2IEwgMC44NTIgLTUuMTM2IEwgMC45IC01LjM4OCBRIDEuMjg0IC01LjU5MiAxLjU0MiAtNS43ODQgUSAxLjggLTUuOTc2IDIuMDM0IC02LjI4OCBRIDIuMjY4IC02LjYgMi41NjggLTcuMTE2IEwgMi45MjggLTcuMTE2IEwgMi41OTIgLTUuNjA0IFogIiAvPjwvc3ltYm9sPjx1c2UgaHJlZj0iI1NUSVhUd29NYXRoUmVndWxhcl8zMzQ3IiB4PSI3OS43MTYiIHk9Ii0xNTIuODUyIiB3aWR0aD0iNC40MzgiIGhlaWdodD0iNTkuNzUyIiBmaWxsPSJ3aGl0ZSIgLz48c3ltYm9sIGlkPSJTVElYVHdvTWF0aFJlZ3VsYXJfMTIwMiIgdmlld0JveD0iMCAtMzEuNTI0IDcuODk2IDUxLjIxNiI+PHBhdGggZD0iTSA3Ljg5NiAtMy45MjQgTCA3Ljg5NiAtMy45MjQgTCAwLjc0NCAtMy45MjQgTCAwLjc0NCAtNC43NCBMIDcuODk2IC00Ljc0IFogTSA3Ljg5NiAtMS40NzYgTCA3Ljg5NiAtMS40NzYgTCAwLjc0NCAtMS40NzYgTCAwLjc0NCAtMi4yOCBMIDcuODk2IC0yLjI4IFogIiAvPjwvc3ltYm9sPjx1c2UgaHJlZj0iI1NUSVhUd29NYXRoUmVndWxhcl8xMjAyIiB4PSI4OS42MzIiIHk9Ii0xNTIuODUyIiB3aWR0aD0iOS4yMTIiIGhlaWdodD0iNTkuNzUyIiBmaWxsPSJ3aGl0ZSIgLz48c3ltYm9sIGlkPSJTVElYVHdvTWF0aFJlZ3VsYXJfMTEzNyIgdmlld0JveD0iMCAtMzEuNTI0IDUuNjE2IDUxLjIxNiI+PHBhdGggZD0iTSA1LjYxNiAtMy44MDQgTCA1LjYxNiAtMy44MDQgUSA1LjYxNiAtMy4wMzYgNS40NjYgLTIuMzI4IFEgNS4zMTYgLTEuNjIgNC45OTggLTEuMDY4IFEgNC42OCAtMC41MTYgNC4xNzYgLTAuMTkyIFEgMy42NzIgMC4xMzIgMi45NjQgMC4xMzIgUSAyLjI0NCAwLjEzMiAxLjc0IC0wLjE4NiBRIDEuMjM2IC0wLjUwNCAwLjkyNCAtMS4wNTYgUSAwLjYxMiAtMS42MDggMC40NjggLTIuMzI4IFEgMC4zMjQgLTMuMDQ4IDAuMzI0IC0zLjg0IFEgMC4zMjQgLTQuOTggMC42MyAtNS44NSBRIDAuOTM2IC02LjcyIDEuNTMgLTcuMjEyIFEgMi4xMjQgLTcuNzA0IDIuOTg4IC03LjcwNCBRIDMuNzY4IC03LjcwNCA0LjM1NiAtNy4yNDIgUSA0Ljk0NCAtNi43OCA1LjI4IC01LjkxIFEgNS42MTYgLTUuMDQgNS42MTYgLTMuODA0IFogTSA0LjQ1MiAtMy43MzIgTCA0LjQ1MiAtMy43MzIgUSA0LjQ1MiAtNS41NTYgNC4wNzQgLTYuNDI2IFEgMy42OTYgLTcuMjk2IDIuOTUyIC03LjI5NiBRIDIuMjQ0IC03LjI5NiAxLjg2IC02LjQzMiBRIDEuNDc2IC01LjU2OCAxLjQ3NiAtMy43OCBRIDEuNDc2IC0yLjA0IDEuODYgLTEuMTcgUSAyLjI0NCAtMC4zIDIuOTY0IC0wLjMgUSAzLjY3MiAtMC4zIDQuMDYyIC0xLjE1MiBRIDQuNDUyIC0yLjAwNCA0LjQ1MiAtMy43MzIgWiAiIC8+PC9zeW1ib2w+PHVzZSBocmVmPSIjU1RJWFR3b01hdGhSZWd1bGFyXzExMzciIHg9IjEwNC44OTYiIHk9Ii0xNTIuODUyIiB3aWR0aD0iNi41NTIiIGhlaWdodD0iNTkuNzUyIiBmaWxsPSJ3aGl0ZSIgLz48L2c+PC9nPjwvZz48Zz48Zz48Zz48c3ltYm9sIGlkPSJTVElYVHdvTWF0aFJlZ3VsYXJfMTEzOCIgdmlld0JveD0iMCAtMzEuNTI0IDUuMTEyIDUxLjIxNiI+PHBhdGggZD0iTSA1LjExMiAwIEwgNS4xMTIgMCBMIDAuOTYgMCBMIDAuOTYgLTAuMzM2IFEgMS44MTIgLTAuMzM2IDIuMTYgLTAuNTE2IFEgMi41MDggLTAuNjk2IDIuNTA4IC0xLjE0IEwgMi41MDggLTYuMjE2IFEgMi41MDggLTYuNTI4IDIuNDI0IC02LjY2NiBRIDIuMzQgLTYuODA0IDIuMTEyIC02LjgwNCBRIDEuOTMyIC02LjgwNCAxLjU0OCAtNi43NTYgUSAxLjE2NCAtNi43MDggMC44ODggLTYuNjEyIEwgMC44ODggLTcuMDA4IEwgMy4yMTYgLTcuNjY4IEwgMy41NjQgLTcuNjY4IEwgMy41NjQgLTEuMTQgUSAzLjU2NCAtMC42OTYgMy45MjQgLTAuNTE2IFEgNC4yODQgLTAuMzM2IDUuMTEyIC0wLjMzNiBaICIgLz48L3N5bWJvbD48dXNlIGhyZWY9IiNTVElYVHdvTWF0aFJlZ3VsYXJfMTEzOCIgeD0iMTQ3LjU2OCIgeT0iLTg2LjE3MiIgd2lkdGg9IjUuOTY0IiBoZWlnaHQ9IjU5Ljc1MiIgZmlsbD0id2hpdGUiIC8+PHN5bWJvbCBpZD0iU1RJWFR3b01hdGhSZWd1bGFyXzExMzciIHZpZXdCb3g9IjAgLTMxLjUyNCA1LjYxNiA1MS4yMTYiPjxwYXRoIGQ9Ik0gNS42MTYgLTMuODA0IEwgNS42MTYgLTMuODA0IFEgNS42MTYgLTMuMDM2IDUuNDY2IC0yLjMyOCBRIDUuMzE2IC0xLjYyIDQuOTk4IC0xLjA2OCBRIDQuNjggLTAuNTE2IDQuMTc2IC0wLjE5MiBRIDMuNjcyIDAuMTMyIDIuOTY0IDAuMTMyIFEgMi4yNDQgMC4xMzIgMS43NCAtMC4xODYgUSAxLjIzNiAtMC41MDQgMC45MjQgLTEuMDU2IFEgMC42MTIgLTEuNjA4IDAuNDY4IC0yLjMyOCBRIDAuMzI0IC0zLjA0OCAwLjMyNCAtMy44NCBRIDAuMzI0IC00Ljk4IDAuNjMgLTUuODUgUSAwLjkzNiAtNi43MiAxLjUzIC03LjIxMiBRIDIuMTI0IC03LjcwNCAyLjk4OCAtNy43MDQgUSAzLjc2OCAtNy43MDQgNC4zNTYgLTcuMjQyIFEgNC45NDQgLTYuNzggNS4yOCAtNS45MSBRIDUuNjE2IC01LjA0IDUuNjE2IC0zLjgwNCBaIE0gNC40NTIgLTMuNzMyIEwgNC40NTIgLTMuNzMyIFEgNC40NTIgLTUuNTU2IDQuMDc0IC02LjQyNiBRIDMuNjk2IC03LjI5NiAyLjk1MiAtNy4yOTYgUSAyLjI0NCAtNy4yOTYgMS44NiAtNi40MzIgUSAxLjQ3NiAtNS41NjggMS40NzYgLTMuNzggUSAxLjQ3NiAtMi4wNCAxLjg2IC0xLjE3IFEgMi4yNDQgLTAuMyAyLjk2NCAtMC4zIFEgMy42NzIgLTAuMyA0LjA2MiAtMS4xNTIgUSA0LjQ1MiAtMi4wMDQgNC40NTIgLTMuNzMyIFogIiAvPjwvc3ltYm9sPjx1c2UgaHJlZj0iI1NUSVhUd29NYXRoUmVndWxhcl8xMTM3IiB4PSIxNTQuNDk4IiB5PSItODYuMTcyIiB3aWR0aD0iNi41NTIiIGhlaWdodD0iNTkuNzUyIiBmaWxsPSJ3aGl0ZSIgLz48dXNlIGhyZWY9IiNTVElYVHdvTWF0aFJlZ3VsYXJfMTEzNyIgeD0iMTYxLjQyOCIgeT0iLTg2LjE3MiIgd2lkdGg9IjYuNTUyIiBoZWlnaHQ9IjU5Ljc1MiIgZmlsbD0id2hpdGUiIC8+PHN5bWJvbCBpZD0iU1RJWFR3b01hdGhSZWd1bGFyXzg2OCIgdmlld0JveD0iMCAtMzEuNTI0IDguNzcyIDUxLjIxNiI+PHBhdGggZD0iTSA4LjQ5NiAwIEwgOC40OTYgMCBMIDUuMzY0IDAgTCA1LjM2NCAtMC44NjQgUSA1LjkyOCAtMS4zMiA2LjMxOCAtMS45NSBRIDYuNzA4IC0yLjU4IDYuOTA2IC0zLjI4MiBRIDcuMTA0IC0zLjk4NCA3LjEwNCAtNC42NDQgUSA3LjEwNCAtNiA2LjQ1NiAtNi43NjIgUSA1LjgwOCAtNy41MjQgNC43MDQgLTcuNTI0IFEgMy41NjQgLTcuNTI0IDIuOTA0IC02Ljc1IFEgMi4yNDQgLTUuOTc2IDIuMjQ0IC00LjY0NCBRIDIuMjQ0IC0zLjk5NiAyLjQzNiAtMy4yODIgUSAyLjYyOCAtMi41NjggMy4wMTggLTEuOTMyIFEgMy40MDggLTEuMjk2IDMuOTg0IC0wLjg1MiBMIDMuOTg0IDAgTCAwLjgxNiAwIEwgMC41NjQgLTIuMTI0IEwgMC45MTIgLTIuMTI0IFEgMS4wMiAtMS43MTYgMS4xMjIgLTEuNDY0IFEgMS4yMjQgLTEuMjEyIDEuNDEgLTEuMDkyIFEgMS41OTYgLTAuOTcyIDEuOTMyIC0wLjk3MiBMIDMuMDI0IC0wLjk3MiBMIDMuMDI0IC0xLjA0NCBRIDIuNjg4IC0xLjI5NiAyLjMyMiAtMS42MjYgUSAxLjk1NiAtMS45NTYgMS42NDQgLTIuMzg4IFEgMS4zMzIgLTIuODIgMS4xNCAtMy4zOSBRIDAuOTQ4IC0zLjk2IDAuOTQ4IC00LjY5MiBRIDAuOTQ4IC01LjY4OCAxLjQyOCAtNi40MzggUSAxLjkwOCAtNy4xODggMi43NiAtNy42MDggUSAzLjYxMiAtOC4wMjggNC43MTYgLTguMDI4IFEgNS44MzIgLTguMDI4IDYuNjYgLTcuNjA4IFEgNy40ODggLTcuMTg4IDcuOTQ0IC02LjQzOCBRIDguNCAtNS42ODggOC40IC00LjcwNCBRIDguNCAtMy45NzIgOC4xOTYgLTMuNDAyIFEgNy45OTIgLTIuODMyIDcuNjc0IC0yLjM5NCBRIDcuMzU2IC0xLjk1NiA3LjAwMiAtMS42MjYgUSA2LjY0OCAtMS4yOTYgNi4zMzYgLTEuMDQ0IEwgNi4zMzYgLTAuOTcyIEwgNy4zNDQgLTAuOTcyIFEgNy43MDQgLTAuOTcyIDcuODkgLTEuMDkyIFEgOC4wNzYgLTEuMjEyIDguMTkgLTEuNDcgUSA4LjMwNCAtMS43MjggOC40MjQgLTIuMTI0IEwgOC43NzIgLTIuMTI0IFogIiAvPjwvc3ltYm9sPjx1c2UgaHJlZj0iI1NUSVhUd29NYXRoUmVndWxhcl84NjgiIHg9IjE2OC4zNTgiIHk9Ii04Ni4xNzIiIHdpZHRoPSIxMC4yMzQiIGhlaWdodD0iNTkuNzUyIiBmaWxsPSJ3aGl0ZSIgLz48L2c+PC9nPjwvZz48Zz48Zz48Zz48c3ltYm9sIGlkPSJTVElYVHdvTWF0aFJlZ3VsYXJfMTE5NiIgdmlld0JveD0iMCAtMzEuNTI0IDcuODk2IDUxLjIxNiI+PHBhdGggZD0iTSA3Ljg5NiAtMi43IEwgNy44OTYgLTIuNyBMIDQuNzUyIC0yLjcgTCA0Ljc1MiAwLjQ4IEwgMy44ODggMC40OCBMIDMuODg4IC0yLjcgTCAwLjc0NCAtMi43IEwgMC43NDQgLTMuNTE2IEwgMy44ODggLTMuNTE2IEwgMy44ODggLTYuNjk2IEwgNC43NTIgLTYuNjk2IEwgNC43NTIgLTMuNTE2IEwgNy44OTYgLTMuNTE2IFogIiAvPjwvc3ltYm9sPjx1c2UgaHJlZj0iI1NUSVhUd29NYXRoUmVndWxhcl8xMTk2IiB4PSIyMDMuNCIgeT0iLTExNC4xNTIiIHdpZHRoPSI5LjIxMiIgaGVpZ2h0PSI1OS43NTIiIGZpbGw9IndoaXRlIiAvPjwvZz48L2c+PC9nPjxnPjxnPjxnPjxzeW1ib2wgaWQ9IlNUSVhUd29NYXRoUmVndWxhcl8zMzUwIiB2aWV3Qm94PSIwIC0zMS41MjQgNS43NDggNTEuMjE2Ij48cGF0aCBkPSJNIDIuNCAtNS43MTIgTCAyLjQgLTUuNzEyIEwgMi42NjQgLTUuNzEyIEwgMi4xIC0zLjQwOCBRIDIuMDE2IC0zLjA5NiAxLjkyNiAtMi42NTggUSAxLjgzNiAtMi4yMiAxLjgzNiAtMS44MTIgUSAxLjgzNiAtMS4zNDQgMi4wMjggLTEuMDAyIFEgMi4yMiAtMC42NiAyLjc3MiAtMC42NiBRIDMuMzg0IC0wLjY2IDMuODI4IC0wLjk1NCBRIDQuMjcyIC0xLjI0OCA0LjU1NCAtMS43MDQgUSA0LjgzNiAtMi4xNiA0Ljk3NCAtMi42NjQgUSA1LjExMiAtMy4xNjggNS4xMTIgLTMuNTg4IFEgNS4xMTIgLTMuOSA1LjAwNCAtNC4xMjggUSA0Ljg5NiAtNC4zNTYgNC43NDYgLTQuNTM2IFEgNC41OTYgLTQuNzE2IDQuNDg4IC00Ljg4NCBRIDQuMzggLTUuMDUyIDQuMzggLTUuMjQ0IFEgNC4zOCAtNS40ODQgNC41MTIgLTUuNjE2IFEgNC42NDQgLTUuNzQ4IDQuODM2IC01Ljc0OCBRIDUuMjQ0IC01Ljc0OCA1LjQ5NiAtNS4zNCBRIDUuNzQ4IC00LjkzMiA1Ljc0OCAtNC4yOTYgUSA1Ljc0OCAtMy43NDQgNS42MSAtMy4xMzggUSA1LjQ3MiAtMi41MzIgNS4xOTYgLTEuOTU2IFEgNC45MiAtMS4zOCA0LjUxMiAtMC45MTIgUSA0LjEwNCAtMC40NDQgMy41NzYgLTAuMTY4IFEgMy4wNDggMC4xMDggMi40IDAuMTA4IFEgMS44IDAuMTA4IDEuNDY0IC0wLjEzMiBRIDEuMTI4IC0wLjM3MiAwLjk5IC0wLjc2OCBRIDAuODUyIC0xLjE2NCAwLjg1MiAtMS42MiBRIDAuODUyIC0yLjA1MiAwLjkzNiAtMi41MDggUSAxLjAyIC0yLjk2NCAxLjExNiAtMy4zMzYgTCAxLjM1NiAtNC4yNDggUSAxLjM4IC00LjM0NCAxLjQyOCAtNC41MzYgUSAxLjQ3NiAtNC43MjggMS40NzYgLTQuOTA4IFEgMS40NzYgLTUuMDY0IDEuNDE2IC01LjE3MiBRIDEuMzU2IC01LjI4IDEuMTg4IC01LjI4IFEgMS4wNDQgLTUuMjggMC44NCAtNS4yNjIgUSAwLjYzNiAtNS4yNDQgMC42MzYgLTUuMjQ0IEwgMC42MzYgLTUuNTggWiAiIC8+PC9zeW1ib2w+PHVzZSBocmVmPSIjU1RJWFR3b01hdGhSZWd1bGFyXzMzNTAiIHg9IjIwMy40IiB5PSItODguOTU0IiB3aWR0aD0iNi43MDYiIGhlaWdodD0iNTkuNzUyIiBmaWxsPSJ3aGl0ZSIgLz48c3ltYm9sIGlkPSJTVElYVHdvTWF0aFJlZ3VsYXJfNDQzOCIgdmlld0JveD0iMCAtMzEuNTI0IDYuMyA1MS4yMTYiPjxwYXRoIGQ9Ik0gNi4zIC0zLjc2OCBMIDYuMyAtMy43NjggUSA2LjMgLTIuNjc2IDUuODUgLTEuNzg4IFEgNS40IC0wLjkgNC41OTYgLTAuMzc4IFEgMy43OTIgMC4xNDQgMi43MjQgMC4xNDQgUSAxLjkwOCAwLjE0NCAxLjQwNCAtMC4yMDQgUSAwLjkgLTAuNTUyIDAuNjY2IC0xLjEwNCBRIDAuNDMyIC0xLjY1NiAwLjQzMiAtMi4yNTYgUSAwLjQzMiAtMy4wMjQgMC42ODQgLTMuNzMyIFEgMC45MzYgLTQuNDQgMS40MDQgLTQuOTk4IFEgMS44NzIgLTUuNTU2IDIuNTIgLTUuODggUSAzLjE2OCAtNi4yMDQgMy45NiAtNi4yMDQgUSA0LjU2IC02LjIwNCA1LjA5NCAtNS45MjIgUSA1LjYyOCAtNS42NCA1Ljk2NCAtNS4wOTQgUSA2LjMgLTQuNTQ4IDYuMyAtMy43NjggWiBNIDUuMDQgLTMuODQgTCA1LjA0IC0zLjg0IFEgNS4wNCAtNC4zNTYgNC45MiAtNC43ODIgUSA0LjggLTUuMjA4IDQuNTQyIC01LjQ2NiBRIDQuMjg0IC01LjcyNCAzLjg1MiAtNS43MjQgUSAzLjI2NCAtNS43MjQgMi43NzIgLTUuMzA0IFEgMi4yOCAtNC44ODQgMS45ODYgLTQuMDk4IFEgMS42OTIgLTMuMzEyIDEuNjkyIC0yLjE4NCBRIDEuNjkyIC0xLjcxNiAxLjc4OCAtMS4yOTYgUSAxLjg4NCAtMC44NzYgMi4xMzYgLTAuNjE4IFEgMi4zODggLTAuMzYgMi44NTYgLTAuMzYgUSAzLjg1MiAtMC4zNiA0LjQ0NiAtMS4yNjYgUSA1LjA0IC0yLjE3MiA1LjA0IC0zLjg0IFogIiAvPjwvc3ltYm9sPjx1c2UgaHJlZj0iI1NUSVhUd29NYXRoUmVndWxhcl80NDM4IiB4PSIyMTAuMTA2IiB5PSItNzQuOTgiIHdpZHRoPSI1LjE0NSIgaGVpZ2h0PSI0MS44MjYiIGZpbGw9IndoaXRlIiAvPjwvZz48L2c+PC9nPjxnPjxnPjxnPjxyZWN0IHg9IjIwMy40IiB5PSItMzEuMDg4IiB3aWR0aD0iMy43OCIgaGVpZ2h0PSIwLjk1MiIgZmlsbD0id2hpdGUiIC8+PC9nPjwvZz48L2c+PGNpcmNsZSBjeD0iOTUuMzk5OTk5OTk5OTk5OTkiIGN5PSItMC4wIiByPSIyLjY5OTk5OTk5OTk5OTk5OTciIHN0eWxlPSJzdHJva2U6d2hpdGU7ZmlsbDp3aGl0ZTtzdHJva2Utd2lkdGg6Mi4wO3N0cm9rZS1kYXNoYXJyYXk6LTsiIC8+PGc+PGc+PGc+PHN5bWJvbCBpZD0iU1RJWFR3b01hdGhSZWd1bGFyXzQyNDQiIHZpZXdCb3g9IjAgLTMxLjUyNCAzLjc1NiA1MS4yMTYiPjxwYXRoIGQ9Ik0gMy43NTYgMCBMIDMuNzU2IDAgTCAyLjc2IDAgTCAyLjc2IC02LjM0OCBMIDEuMjEyIC02LjM0OCBMIDEuMjEyIC02Ljk5NiBRIDEuODQ4IC03LjA5MiAyLjI1NiAtNy4yMyBRIDIuNjY0IC03LjM2OCAzIC03LjU2IEwgMy43NTYgLTcuNTYgWiAiIC8+PC9zeW1ib2w+PHVzZSBocmVmPSIjU1RJWFR3b01hdGhSZWd1bGFyXzQyNDQiIHg9IjYyLjQwMiIgeT0iLTczLjk1NiIgd2lkdGg9IjQuMzgyIiBoZWlnaHQ9IjU5Ljc1MiIgZmlsbD0id2hpdGUiIC8+PC9nPjxnPjxzeW1ib2wgaWQ9IlNUSVhUd29NYXRoUmVndWxhcl80MDI2IiB2aWV3Qm94PSItMC4yODggLTMxLjUyNCA2Ljg4OCA1MS4yMTYiPjxwYXRoIGQ9Ik0gNi4zMzYgLTUuNTY4IEwgNi4zMzYgLTUuNTY4IEwgNS4xNiAtMi42MDQgUSA1LjE2IC0yLjU1NiA1LjEyNCAtMi4yOTggUSA1LjA4OCAtMi4wNCA1LjA4OCAtMS44MzYgUSA1LjA4OCAtMS4yNDggNS4yMjYgLTEuMDIgUSA1LjM2NCAtMC43OTIgNS42MjggLTAuNzkyIFEgNS45MTYgLTAuNzkyIDYuMDg0IC0wLjk0OCBRIDYuMjUyIC0xLjEwNCA2LjQwOCAtMS4yNzIgTCA2LjYgLTEuMDY4IFEgNi40NTYgLTAuNzkyIDYuMjUyIC0wLjUyMiBRIDYuMDQ4IC0wLjI1MiA1Ljc2NiAtMC4wNzggUSA1LjQ4NCAwLjA5NiA1LjA4OCAwLjA5NiBRIDQuNjQ0IDAuMDk2IDQuNDcgLTAuMTc0IFEgNC4yOTYgLTAuNDQ0IDQuMjk2IC0wLjg2NCBRIDQuMjk2IC0xLjAzMiA0LjMyIC0xLjI2IFEgNC4zNDQgLTEuNDg4IDQuMzkyIC0xLjk2OCBMIDQuMzIgLTEuOTY4IFEgMy45NiAtMS4wOTIgMy42IC0wLjYzIFEgMy4yNCAtMC4xNjggMi45MSAtMC4wMDYgUSAyLjU4IDAuMTU2IDIuMjggMC4xNTYgUSAxLjk1NiAwLjE1NiAxLjc2NCAtMC4wMTggUSAxLjU3MiAtMC4xOTIgMS40NzYgLTAuNDI2IFEgMS4zOCAtMC42NiAxLjMzMiAtMC44MjggTCAxLjI3MiAtMC44MjggUSAxLjEyOCAtMC4wOTYgMS4wOTIgMC41MjggUSAxLjA1NiAxLjE1MiAwLjg4OCAxLjgyNCBRIDAuNzY4IDIuMzE2IDAuNDc0IDIuNDYgUSAwLjE4IDIuNjA0IC0wLjEyIDIuNjA0IEwgLTAuMjg4IDIuNTY4IFEgLTAuMDk2IDEuNzUyIDAuMDcyIDEuMDYyIFEgMC4yNCAwLjM3MiAwLjQ1IC0wLjMxMiBRIDAuNjYgLTAuOTk2IDAuOTM2IC0xLjc4OCBRIDEuMDMyIC0yLjI2OCAxLjE0IC0yLjgyNiBRIDEuMjQ4IC0zLjM4NCAxLjM1IC0zLjkzIFEgMS40NTIgLTQuNDc2IDEuNTQyIC00LjkyNiBRIDEuNjMyIC01LjM3NiAxLjcwNCAtNS42NCBMIDIuNzg0IC01LjY3NiBMIDIuODQ0IC01LjU2OCBMIDEuNzUyIC0yLjYwNCBRIDEuNzUyIC0yLjYwNCAxLjcxNiAtMi40NDIgUSAxLjY4IC0yLjI4IDEuNjggLTIuMDQgUSAxLjY4IC0xLjU5NiAxLjg2NiAtMS4yMTggUSAyLjA1MiAtMC44NCAyLjQ5NiAtMC44NCBRIDIuODIgLTAuODQgMy4xMzIgLTEuMDY4IFEgMy40NDQgLTEuMjk2IDMuNzIgLTEuNjQ0IFEgMy45OTYgLTEuOTkyIDQuMjA2IC0yLjM1MiBRIDQuNDE2IC0yLjcxMiA0LjU0MiAtMyBRIDQuNjY4IC0zLjI4OCA0LjY5MiAtMy4zNzIgUSA0LjggLTMuODY0IDQuOTM4IC00LjUwNiBRIDUuMDc2IC01LjE0OCA1LjIyIC01LjY0IEwgNi4yNzYgLTUuNjc2IFogIiAvPjwvc3ltYm9sPjx1c2UgaHJlZj0iI1NUSVhUd29NYXRoUmVndWxhcl80MDI2IiB4PSI2OC4xIiB5PSItNzMuOTU2IiB3aWR0aD0iOC4wMzYiIGhlaWdodD0iNTkuNzUyIiBmaWxsPSJ3aGl0ZSIgLz48L2c+PGc+PHN5bWJvbCBpZD0iU1RJWFR3b01hdGhSZWd1bGFyXzM2NjQiIHZpZXdCb3g9IjAgLTMxLjUyNCA1Ljc0OCA1MS4yMTYiPjxwYXRoIGQ9Ik0gMS4xMDQgMCBMIDEuMTA0IDAgTCAxLjEwNCAtNy44ODQgTCA1Ljc0OCAtNy44ODQgTCA1Ljc0OCAtNy4wMiBMIDIuMTM2IC03LjAyIEwgMi4xMzYgLTQuMzggTCA1LjE5NiAtNC4zOCBMIDUuMTk2IC0zLjUxNiBMIDIuMTM2IC0zLjUxNiBMIDIuMTM2IDAgWiAiIC8+PC9zeW1ib2w+PHVzZSBocmVmPSIjU1RJWFR3b01hdGhSZWd1bGFyXzM2NjQiIHg9Ijc3LjAwNCIgeT0iLTczLjk1NiIgd2lkdGg9IjYuNzA2IiBoZWlnaHQ9IjU5Ljc1MiIgZmlsbD0id2hpdGUiIC8+PC9nPjwvZz48L2c+PC9zdmc+\" alt=\"Analog Circuit\" title=\"\" \/><\/p>\n<details>\n  <summary>Click to expand all the Schemdraw-Markdown Goodness!<\/summary>\n\n<div class=\"highlight\"><pre><span><\/span><code>::_schemdraw_:: alt=&quot;Analog Circuit&quot; color=&quot;white&quot;\n    (V1 := elm.SourceV().label(&#39;5V&#39;))\n    elm.Line().right(drawing.unit*.75)\n    (S1 := elm.SwitchSpdt2(action=&#39;close&#39;).up().anchor(&#39;b&#39;).label(&#39;$t=0$&#39;, loc=&#39;rgt&#39;))\n    elm.Line().right(drawing.unit*.75).at(S1.c)\n    elm.Resistor().down().label(&#39;$100\\\\Omega$&#39;).label([&#39;+&#39;,&#39;$v_o$&#39;,&#39;-&#39;], loc=&#39;bot&#39;)\n    elm.Line().to(V1.start)\n    elm.Capacitor().at(S1.a).toy(V1.start).label(&#39;1$\\\\mu$F&#39;).dot()\n::end-schemdraw::\n<\/code><\/pre><\/div>\n\n\n<\/details>\n\n<h2>How Does it Work?<\/h2>\n<p>In a nutshell, we use some regular expressions to slurp the specially formatted section out of the markdown, and then we do some conditioning for the \"instructions\"\nand punch them into a custom <code>exec<\/code> block. Yeah, that's right, the dreaded exec block. I'm not particularly pleased about it, either, but it could be worse...<\/p>\n<blockquote>\n<p><strong><em>right?<\/em><\/strong><\/p>\n<\/blockquote>\n<p><img src=\"https:\/\/blog.stanleysolutionsnw.com\/techy-granny.jpg\" width=\"100%\" alt=\"Granny's got it...\"><\/p>\n<p>At any rate, there's no sense crying over that spilled milk, for the time-being, it's simple enough, and doesn't cause too much trouble. I clearly call out a\nwarning in the README docs for the new package. And speaking of the \"new package,\" it's already on <a href=\"https:\/\/pypi.org\/project\/schemdraw-markdown\/\">PyPI<\/a>.<\/p>\n<p>To get started with it in your documentation, just go ahead and...<\/p>\n<div class=\"highlight\"><pre><span><\/span><code>$<span class=\"w\"> <\/span>pip3<span class=\"w\"> <\/span>install<span class=\"w\"> <\/span>schemdraw-markdown\n<\/code><\/pre><\/div>\n\n<p>And let me know what you think in the comments below!<\/p>","category":[{"@attributes":{"term":"Python"}},{"@attributes":{"term":"markdown"}},{"@attributes":{"term":"pelican"}},{"@attributes":{"term":"blogging"}},{"@attributes":{"term":"circuits"}},{"@attributes":{"term":"schematics"}},{"@attributes":{"term":"python"}}]},{"title":"Telnetlib, Python, and SEL Protocol","link":{"@attributes":{"href":"https:\/\/blog.stanleysolutionsnw.com\/telnetlib-python-and-sel-protocol.html","rel":"alternate"}},"published":"2022-01-26T16:32:00-08:00","updated":"2022-01-26T16:32:00-08:00","author":{"name":"Joe Stanley"},"id":"tag:blog.stanleysolutionsnw.com,2022-01-26:\/telnetlib-python-and-sel-protocol.html","summary":"<p>Python's telnetlib library doesn't like null-characters, SEL protocol does, that makes for some interesting challenges.<\/p>","content":"<h3>Zeros don't matter, right?<\/h3>\n<p>Well, that depends on who, (<em>or what<\/em>) you're talking to. If it happens to be an SEL relay, those zeros are pretty important.<\/p>\n<p>How important? Well, every byte tells a story... so that means that every zero is important.<\/p>\n<h3>Where did this start?<\/h3>\n<p>We can take a few steps back. Go take a look at <a href=\"\/reading-data-with-selprotopy.html\">my article introducing SELProtoPy<\/a> to see\nwhat the buzz is all about. In short, I want to write a full protocol client in Python for SEL Fast-Meter, and potentially branch\nout from there. We'll see how far I can take it.<\/p>\n<p>This is definitely an article that I've been wanting to write for some time, because it's very fascinating. Last year, I started\nworking on the project and began writing the protocol parser from specifications provided by an\n<a href=\"https:\/\/selinc.com\/api\/download\/5026\/\">SEL application guide<\/a> which describes the intricacies of the binary SEL Fast-Meter\nprotocol.<\/p>\n<p>Before I describe the challenges in too much detail, however, perhaps I should summarize SEL protocol...<\/p>\n<hr>\n<blockquote>\n<p>When we talk about SEL protocol, we're really discussing a <em>suite<\/em> of protocols which includes:<\/p>\n<ul>\n<li>Fast Meter<\/li>\n<li>Fast Message<\/li>\n<li>Fast Operate<\/li>\n<\/ul>\n<p>Those \"protocols\" are all very closely linked, and are all intended to be \"described\" protocols. In other words, SEL protocol is\nself-describing. It essentially defines one \"main\" command\/response sequence which then provides the definition for each of the\nvarious sub-protocols. SEL protocol commands all start with the hexadecimal-encoded byte <code>A5<\/code>. Each command is two bytes in length,\nand the \"device definition\" command will return a definition of all the other available commands. That is, a device wishing to\nquery an SEL protocol enabled device would issue the hexadecimal string: <code>A5 C0<\/code> and interpret the response to determine what other\ncommands are available for the device. The response from the device definition command not only provides a listing of what commands\nare supported, but what hexadecimal string is required to query for those commands.<\/p>\n<\/blockquote>\n<hr>\n<p>So with the basics recounted, where this gets interesting comes into play when we account for the fact that SEL protocol was originally\na serial-based protocol, and made extensive use of null-padding, which is the practice of using zeros to separate content to account\nfor reasonable byte-alignment. That means that in almost every command response, there's a significant number of null characters (zeros)\nin not only the definitions, but the data regions provided.<\/p>\n<p>The application-guide I mentioned earlier has become something of the \"de facto standard\" for the protocol suite, and it defines how\nSEL protocol provides the numerical quantities used to describe the power system. In many cases, it's possible for those quantities\nto be zero for analog measurements. Whats more, SEL protocol provides an extensive set of word-bits (boolean points) in a bit-packed\nformat (8 word-bits packed into a single byte).<\/p>\n<p>Let's pause and think about that for a moment.<\/p>\n<p>Eight boolean statuses packed into a byte; many dozens, if not hundreds, of bits packed into bytes for a single response. At least a\n50\/50 chance that each bit will be a 0 (false\/deasserted). This all means that it's highly likely that one or more of the bytes will\nbe all 0's... a null-character.<\/p>\n<p>When I was working on this project, pretty early on, I found something interesting; when I would issue commands to request fast-meter\ndata, I'd see that the total data length was significantly shorter than what the response message indicated it should be. After\ndigging in, and looking at a little Wireshark, I found that my usage of Python's <code>telnetlib<\/code> was effectively cutting the null\ncharacters out.<\/p>\n<p>A little googling, and it was further confirmed from an <a href=\"https:\/\/stackoverflow.com\/a\/32616342\/10406011\">answer on StackOverflow<\/a><\/p>\n<h4><em>\"<\/em><\/h4>\n<blockquote>\n<p>I stumbled in this same problem when trying to get data from an RS232-TCP\/IP Converter using telnet - the telnetlib would suppress every 0x00 from the message. As Fredrik Johansson well answered, it is the way telnetlib was implemented.<\/p>\n<\/blockquote>\n<div style=\"text-align: right\">\n<h4><i>\"<\/i><\/h4>\n<\/div>\n\n<p>Luckily enough, there's a fantastic way to resolve this problem, you can actually play a few games with <code>telnetlib<\/code> to monkey-patch\nfunctionality to retain null characters. Just check out this code snippet from that StackOverflow answer:<\/p>\n<div class=\"highlight\"><pre><span><\/span><code><span class=\"kn\">import<\/span> <span class=\"nn\">telnetlib<\/span>\n<span class=\"kn\">from<\/span> <span class=\"nn\">telnetlib<\/span> <span class=\"kn\">import<\/span> <span class=\"n\">IAC<\/span><span class=\"p\">,<\/span> <span class=\"n\">DO<\/span><span class=\"p\">,<\/span> <span class=\"n\">DONT<\/span><span class=\"p\">,<\/span> <span class=\"n\">WILL<\/span><span class=\"p\">,<\/span> <span class=\"n\">WONT<\/span><span class=\"p\">,<\/span> <span class=\"n\">SE<\/span><span class=\"p\">,<\/span> <span class=\"n\">NOOPT<\/span>\n\n<span class=\"k\">def<\/span> <span class=\"nf\">_process_rawq<\/span><span class=\"p\">(<\/span><span class=\"bp\">self<\/span><span class=\"p\">):<\/span>\n<span class=\"w\">    <\/span><span class=\"sd\">&quot;&quot;&quot;Altera\u00e7\u00e3o da implementa\u00e7\u00e3o desta fun\u00e7\u00e3o necess\u00e1ria pois telnetlib suprime 0x00 e \\021 dos dados lidos<\/span>\n<span class=\"sd\">    &quot;&quot;&quot;<\/span>\n    <span class=\"n\">buf<\/span> <span class=\"o\">=<\/span> <span class=\"p\">[<\/span><span class=\"s1\">&#39;&#39;<\/span><span class=\"p\">,<\/span> <span class=\"s1\">&#39;&#39;<\/span><span class=\"p\">]<\/span>\n    <span class=\"k\">try<\/span><span class=\"p\">:<\/span>\n        <span class=\"k\">while<\/span> <span class=\"bp\">self<\/span><span class=\"o\">.<\/span><span class=\"n\">rawq<\/span><span class=\"p\">:<\/span>\n            <span class=\"n\">c<\/span> <span class=\"o\">=<\/span> <span class=\"bp\">self<\/span><span class=\"o\">.<\/span><span class=\"n\">rawq_getchar<\/span><span class=\"p\">()<\/span>\n            <span class=\"k\">if<\/span> <span class=\"ow\">not<\/span> <span class=\"bp\">self<\/span><span class=\"o\">.<\/span><span class=\"n\">iacseq<\/span><span class=\"p\">:<\/span>\n<span class=\"c1\">#                if c == theNULL:<\/span>\n<span class=\"c1\">#                    continue<\/span>\n<span class=\"c1\">#                if c == &quot;\\021&quot;:<\/span>\n<span class=\"c1\">#                    continue<\/span>\n                <span class=\"k\">if<\/span> <span class=\"n\">c<\/span> <span class=\"o\">!=<\/span> <span class=\"n\">IAC<\/span><span class=\"p\">:<\/span>\n                    <span class=\"n\">buf<\/span><span class=\"p\">[<\/span><span class=\"bp\">self<\/span><span class=\"o\">.<\/span><span class=\"n\">sb<\/span><span class=\"p\">]<\/span> <span class=\"o\">=<\/span> <span class=\"n\">buf<\/span><span class=\"p\">[<\/span><span class=\"bp\">self<\/span><span class=\"o\">.<\/span><span class=\"n\">sb<\/span><span class=\"p\">]<\/span> <span class=\"o\">+<\/span> <span class=\"n\">c<\/span>\n                    <span class=\"k\">continue<\/span>\n                <span class=\"k\">else<\/span><span class=\"p\">:<\/span>\n                    <span class=\"bp\">self<\/span><span class=\"o\">.<\/span><span class=\"n\">iacseq<\/span> <span class=\"o\">+=<\/span> <span class=\"n\">c<\/span>\n            <span class=\"k\">elif<\/span> <span class=\"nb\">len<\/span><span class=\"p\">(<\/span><span class=\"bp\">self<\/span><span class=\"o\">.<\/span><span class=\"n\">iacseq<\/span><span class=\"p\">)<\/span> <span class=\"o\">==<\/span> <span class=\"mi\">1<\/span><span class=\"p\">:<\/span>\n                <span class=\"c1\"># &#39;IAC: IAC CMD [OPTION only for WILL\/WONT\/DO\/DONT]&#39;<\/span>\n                <span class=\"k\">if<\/span> <span class=\"n\">c<\/span> <span class=\"ow\">in<\/span> <span class=\"p\">(<\/span><span class=\"n\">DO<\/span><span class=\"p\">,<\/span> <span class=\"n\">DONT<\/span><span class=\"p\">,<\/span> <span class=\"n\">WILL<\/span><span class=\"p\">,<\/span> <span class=\"n\">WONT<\/span><span class=\"p\">):<\/span>\n                    <span class=\"bp\">self<\/span><span class=\"o\">.<\/span><span class=\"n\">iacseq<\/span> <span class=\"o\">+=<\/span> <span class=\"n\">c<\/span>\n                    <span class=\"k\">continue<\/span>\n\n                <span class=\"bp\">self<\/span><span class=\"o\">.<\/span><span class=\"n\">iacseq<\/span> <span class=\"o\">=<\/span> <span class=\"s1\">&#39;&#39;<\/span>\n                <span class=\"k\">if<\/span> <span class=\"n\">c<\/span> <span class=\"o\">==<\/span> <span class=\"n\">IAC<\/span><span class=\"p\">:<\/span>\n                    <span class=\"n\">buf<\/span><span class=\"p\">[<\/span><span class=\"bp\">self<\/span><span class=\"o\">.<\/span><span class=\"n\">sb<\/span><span class=\"p\">]<\/span> <span class=\"o\">=<\/span> <span class=\"n\">buf<\/span><span class=\"p\">[<\/span><span class=\"bp\">self<\/span><span class=\"o\">.<\/span><span class=\"n\">sb<\/span><span class=\"p\">]<\/span> <span class=\"o\">+<\/span> <span class=\"n\">c<\/span>\n                <span class=\"k\">else<\/span><span class=\"p\">:<\/span>\n                    <span class=\"k\">if<\/span> <span class=\"n\">c<\/span> <span class=\"o\">==<\/span> <span class=\"n\">SB<\/span><span class=\"p\">:<\/span> <span class=\"c1\"># SB ... SE start.<\/span>\n                        <span class=\"bp\">self<\/span><span class=\"o\">.<\/span><span class=\"n\">sb<\/span> <span class=\"o\">=<\/span> <span class=\"mi\">1<\/span>\n                        <span class=\"bp\">self<\/span><span class=\"o\">.<\/span><span class=\"n\">sbdataq<\/span> <span class=\"o\">=<\/span> <span class=\"s1\">&#39;&#39;<\/span>\n                    <span class=\"k\">elif<\/span> <span class=\"n\">c<\/span> <span class=\"o\">==<\/span> <span class=\"n\">SE<\/span><span class=\"p\">:<\/span>\n                        <span class=\"bp\">self<\/span><span class=\"o\">.<\/span><span class=\"n\">sb<\/span> <span class=\"o\">=<\/span> <span class=\"mi\">0<\/span>\n                        <span class=\"bp\">self<\/span><span class=\"o\">.<\/span><span class=\"n\">sbdataq<\/span> <span class=\"o\">=<\/span> <span class=\"bp\">self<\/span><span class=\"o\">.<\/span><span class=\"n\">sbdataq<\/span> <span class=\"o\">+<\/span> <span class=\"n\">buf<\/span><span class=\"p\">[<\/span><span class=\"mi\">1<\/span><span class=\"p\">]<\/span>\n                        <span class=\"n\">buf<\/span><span class=\"p\">[<\/span><span class=\"mi\">1<\/span><span class=\"p\">]<\/span> <span class=\"o\">=<\/span> <span class=\"s1\">&#39;&#39;<\/span>\n                    <span class=\"k\">if<\/span> <span class=\"bp\">self<\/span><span class=\"o\">.<\/span><span class=\"n\">option_callback<\/span><span class=\"p\">:<\/span>\n                        <span class=\"c1\"># Callback is supposed to look into<\/span>\n                        <span class=\"c1\"># the sbdataq<\/span>\n                        <span class=\"bp\">self<\/span><span class=\"o\">.<\/span><span class=\"n\">option_callback<\/span><span class=\"p\">(<\/span><span class=\"bp\">self<\/span><span class=\"o\">.<\/span><span class=\"n\">sock<\/span><span class=\"p\">,<\/span> <span class=\"n\">c<\/span><span class=\"p\">,<\/span> <span class=\"n\">NOOPT<\/span><span class=\"p\">)<\/span>\n                    <span class=\"k\">else<\/span><span class=\"p\">:<\/span>\n                        <span class=\"c1\"># We can&#39;t offer automatic processing of<\/span>\n                        <span class=\"c1\"># suboptions. Alas, we should not get any<\/span>\n                        <span class=\"c1\"># unless we did a WILL\/DO before.<\/span>\n                        <span class=\"bp\">self<\/span><span class=\"o\">.<\/span><span class=\"n\">msg<\/span><span class=\"p\">(<\/span><span class=\"s1\">&#39;IAC <\/span><span class=\"si\">%d<\/span><span class=\"s1\"> not recognized&#39;<\/span> <span class=\"o\">%<\/span> <span class=\"nb\">ord<\/span><span class=\"p\">(<\/span><span class=\"n\">c<\/span><span class=\"p\">))<\/span>\n            <span class=\"k\">elif<\/span> <span class=\"nb\">len<\/span><span class=\"p\">(<\/span><span class=\"bp\">self<\/span><span class=\"o\">.<\/span><span class=\"n\">iacseq<\/span><span class=\"p\">)<\/span> <span class=\"o\">==<\/span> <span class=\"mi\">2<\/span><span class=\"p\">:<\/span>\n                <span class=\"n\">cmd<\/span> <span class=\"o\">=<\/span> <span class=\"bp\">self<\/span><span class=\"o\">.<\/span><span class=\"n\">iacseq<\/span><span class=\"p\">[<\/span><span class=\"mi\">1<\/span><span class=\"p\">]<\/span>\n                <span class=\"bp\">self<\/span><span class=\"o\">.<\/span><span class=\"n\">iacseq<\/span> <span class=\"o\">=<\/span> <span class=\"s1\">&#39;&#39;<\/span>\n                <span class=\"n\">opt<\/span> <span class=\"o\">=<\/span> <span class=\"n\">c<\/span>\n                <span class=\"k\">if<\/span> <span class=\"n\">cmd<\/span> <span class=\"ow\">in<\/span> <span class=\"p\">(<\/span><span class=\"n\">DO<\/span><span class=\"p\">,<\/span> <span class=\"n\">DONT<\/span><span class=\"p\">):<\/span>\n                    <span class=\"bp\">self<\/span><span class=\"o\">.<\/span><span class=\"n\">msg<\/span><span class=\"p\">(<\/span><span class=\"s1\">&#39;IAC <\/span><span class=\"si\">%s<\/span><span class=\"s1\"> <\/span><span class=\"si\">%d<\/span><span class=\"s1\">&#39;<\/span><span class=\"p\">,<\/span>\n                        <span class=\"n\">cmd<\/span> <span class=\"o\">==<\/span> <span class=\"n\">DO<\/span> <span class=\"ow\">and<\/span> <span class=\"s1\">&#39;DO&#39;<\/span> <span class=\"ow\">or<\/span> <span class=\"s1\">&#39;DONT&#39;<\/span><span class=\"p\">,<\/span> <span class=\"nb\">ord<\/span><span class=\"p\">(<\/span><span class=\"n\">opt<\/span><span class=\"p\">))<\/span>\n                    <span class=\"k\">if<\/span> <span class=\"bp\">self<\/span><span class=\"o\">.<\/span><span class=\"n\">option_callback<\/span><span class=\"p\">:<\/span>\n                        <span class=\"bp\">self<\/span><span class=\"o\">.<\/span><span class=\"n\">option_callback<\/span><span class=\"p\">(<\/span><span class=\"bp\">self<\/span><span class=\"o\">.<\/span><span class=\"n\">sock<\/span><span class=\"p\">,<\/span> <span class=\"n\">cmd<\/span><span class=\"p\">,<\/span> <span class=\"n\">opt<\/span><span class=\"p\">)<\/span>\n                    <span class=\"k\">else<\/span><span class=\"p\">:<\/span>\n                        <span class=\"bp\">self<\/span><span class=\"o\">.<\/span><span class=\"n\">sock<\/span><span class=\"o\">.<\/span><span class=\"n\">sendall<\/span><span class=\"p\">(<\/span><span class=\"n\">IAC<\/span> <span class=\"o\">+<\/span> <span class=\"n\">WONT<\/span> <span class=\"o\">+<\/span> <span class=\"n\">opt<\/span><span class=\"p\">)<\/span>\n                <span class=\"k\">elif<\/span> <span class=\"n\">cmd<\/span> <span class=\"ow\">in<\/span> <span class=\"p\">(<\/span><span class=\"n\">WILL<\/span><span class=\"p\">,<\/span> <span class=\"n\">WONT<\/span><span class=\"p\">):<\/span>\n                    <span class=\"bp\">self<\/span><span class=\"o\">.<\/span><span class=\"n\">msg<\/span><span class=\"p\">(<\/span><span class=\"s1\">&#39;IAC <\/span><span class=\"si\">%s<\/span><span class=\"s1\"> <\/span><span class=\"si\">%d<\/span><span class=\"s1\">&#39;<\/span><span class=\"p\">,<\/span>\n                        <span class=\"n\">cmd<\/span> <span class=\"o\">==<\/span> <span class=\"n\">WILL<\/span> <span class=\"ow\">and<\/span> <span class=\"s1\">&#39;WILL&#39;<\/span> <span class=\"ow\">or<\/span> <span class=\"s1\">&#39;WONT&#39;<\/span><span class=\"p\">,<\/span> <span class=\"nb\">ord<\/span><span class=\"p\">(<\/span><span class=\"n\">opt<\/span><span class=\"p\">))<\/span>\n                    <span class=\"k\">if<\/span> <span class=\"bp\">self<\/span><span class=\"o\">.<\/span><span class=\"n\">option_callback<\/span><span class=\"p\">:<\/span>\n                        <span class=\"bp\">self<\/span><span class=\"o\">.<\/span><span class=\"n\">option_callback<\/span><span class=\"p\">(<\/span><span class=\"bp\">self<\/span><span class=\"o\">.<\/span><span class=\"n\">sock<\/span><span class=\"p\">,<\/span> <span class=\"n\">cmd<\/span><span class=\"p\">,<\/span> <span class=\"n\">opt<\/span><span class=\"p\">)<\/span>\n                    <span class=\"k\">else<\/span><span class=\"p\">:<\/span>\n                        <span class=\"bp\">self<\/span><span class=\"o\">.<\/span><span class=\"n\">sock<\/span><span class=\"o\">.<\/span><span class=\"n\">sendall<\/span><span class=\"p\">(<\/span><span class=\"n\">IAC<\/span> <span class=\"o\">+<\/span> <span class=\"n\">DONT<\/span> <span class=\"o\">+<\/span> <span class=\"n\">opt<\/span><span class=\"p\">)<\/span>\n    <span class=\"k\">except<\/span> <span class=\"ne\">EOFError<\/span><span class=\"p\">:<\/span> <span class=\"c1\"># raised by self.rawq_getchar()<\/span>\n        <span class=\"bp\">self<\/span><span class=\"o\">.<\/span><span class=\"n\">iacseq<\/span> <span class=\"o\">=<\/span> <span class=\"s1\">&#39;&#39;<\/span> <span class=\"c1\"># Reset on EOF<\/span>\n        <span class=\"bp\">self<\/span><span class=\"o\">.<\/span><span class=\"n\">sb<\/span> <span class=\"o\">=<\/span> <span class=\"mi\">0<\/span>\n        <span class=\"k\">pass<\/span>\n    <span class=\"bp\">self<\/span><span class=\"o\">.<\/span><span class=\"n\">cookedq<\/span> <span class=\"o\">=<\/span> <span class=\"bp\">self<\/span><span class=\"o\">.<\/span><span class=\"n\">cookedq<\/span> <span class=\"o\">+<\/span> <span class=\"n\">buf<\/span><span class=\"p\">[<\/span><span class=\"mi\">0<\/span><span class=\"p\">]<\/span>\n    <span class=\"bp\">self<\/span><span class=\"o\">.<\/span><span class=\"n\">sbdataq<\/span> <span class=\"o\">=<\/span> <span class=\"bp\">self<\/span><span class=\"o\">.<\/span><span class=\"n\">sbdataq<\/span> <span class=\"o\">+<\/span> <span class=\"n\">buf<\/span><span class=\"p\">[<\/span><span class=\"mi\">1<\/span><span class=\"p\">]<\/span>\n<span class=\"n\">telnetlib<\/span><span class=\"o\">.<\/span><span class=\"n\">Telnet<\/span><span class=\"o\">.<\/span><span class=\"n\">process_rawq<\/span> <span class=\"o\">=<\/span> <span class=\"n\">_process_rawq<\/span>\n<\/code><\/pre><\/div>\n\n<p><em>Very interesting........<\/em><\/p>\n<h3>Parting Thoughts<\/h3>\n<p>This is one problem solved, but I've come across an interesting issue where sending the commands over Telnet is not\nworking, but sending the same commands over a plain TCP socket works without failure. Hmm... Weird. That one's\ngoing to take some more research. I'll be sure to post what I find, when I find it!<\/p>\n<p>If you have questions, thoughts, or just want to say \"hi\", feel free to drop me a note in my new comments system\nbelow!<\/p>","category":[{"@attributes":{"term":"Python"}},{"@attributes":{"term":"python"}},{"@attributes":{"term":"sel"}},{"@attributes":{"term":"communications"}},{"@attributes":{"term":"telnet"}},{"@attributes":{"term":"libraries"}},{"@attributes":{"term":"monkey-patch"}}]},{"title":"Automating Python Releases with GitHub Actions","link":{"@attributes":{"href":"https:\/\/blog.stanleysolutionsnw.com\/automating-python-releases-with-github-actions.html","rel":"alternate"}},"published":"2021-11-25T14:01:00-08:00","updated":"2021-11-25T14:01:00-08:00","author":{"name":"Joe Stanley"},"id":"tag:blog.stanleysolutionsnw.com,2021-11-25:\/automating-python-releases-with-github-actions.html","summary":"<p>I'm pretty lazy... We've covered that already, but wouldn't it be exceptionally nice if I could make GitHub automate Python package releases for me? Lets do that...<\/p>","content":"<p>Oh yes. I'm lazy.<\/p>\n<p>Haven't we established that, yet? Well, we're going to hit that nail home with this topic...<\/p>\n<p>We've established previously that I manage a number of random Python packages, including <em>ElectricPy<\/em>, <em>SELProtoPy<\/em>, and <em>PyCEV<\/em>.\nI've come to the realization that I need all the help I can get with releasing updates on a regular basis. So... How shall we do\nthat?<\/p>\n<h3>Where to Start?<\/h3>\n<p>I decided that I needed this for <em>ElectricPy<\/em> first. So let's start with figuring out what we want to do:<\/p>\n<ul>\n<li>Identify the Current ElectricPy Version from the Source Code (bail out if the version is the same or older than what's previously been released)<\/li>\n<li>Create a Tag that Matches the ElectricPy Version, then Push that to GitHub<\/li>\n<li>Build the Python Package as a Source-Code Bundle, and as a <a href=\"https:\/\/pythonwheels.com\">Python Wheel<\/a><\/li>\n<li>Push the Packages to the Python Package Index (PyPI)<\/li>\n<\/ul>\n<p>So those are the primary requirements. Now, let's work out how we're going to do it. I know that I want to use GitHub actions to\ndo this whole thing. So let's start there.<\/p>\n<h3>What are GitHub Actions, Anyway?<\/h3>\n<p>Well, GitHub actions are GitHub's way of providing CI\/CD systems. Essentially, providing Linux-container based workflows that are\ndefined through YAML description files. The YAML (which stands for Yet Another Mark-up Language) files define what container base\nshould be used, and what the steps need to be completed.<\/p>\n<h3>Are there any Read-to-Go GitHub Actions?<\/h3>\n<p>Well, yes... But, actually no.<\/p>\n<p>There's quite a few pretty good actions available in the community, but getting everything <em>just<\/em> right is a bit more tricky. Why, you\nask? Well, they all completed one or two of those actions I'd outlined above, but they didn't cover the whole list. So, I decided to\nglue them all together with a bit of Python!<\/p>\n<h3>Start by Identifying the Version<\/h3>\n<p>We need to pick the version out of the ElectricPy package, and then we need to double-check that it's not already used, or older than\nthe most-up-to-date version. So I built a simple little script:<\/p>\n<div class=\"highlight\"><pre><span><\/span><code><span class=\"c1\"># Release Versioning Support Script<\/span>\n<span class=\"c1\"># Joe Stanley | 2021<\/span>\n\n<span class=\"kn\">import<\/span> <span class=\"nn\">requests<\/span>\n\n<span class=\"n\">USERNAME<\/span> <span class=\"o\">=<\/span> <span class=\"s1\">&#39;engineerjoe440&#39;<\/span>\n<span class=\"n\">REPO<\/span> <span class=\"o\">=<\/span> <span class=\"s1\">&#39;electricpy&#39;<\/span>\n\n<span class=\"k\">try<\/span><span class=\"p\">:<\/span>\n    <span class=\"kn\">import<\/span> <span class=\"nn\">electricpy<\/span> <span class=\"k\">as<\/span> <span class=\"nn\">ep<\/span>\n<span class=\"k\">except<\/span> <span class=\"ne\">ImportError<\/span><span class=\"p\">:<\/span>\n    <span class=\"kn\">import<\/span> <span class=\"nn\">os<\/span><span class=\"o\">,<\/span> <span class=\"nn\">sys<\/span>\n    <span class=\"n\">sys<\/span><span class=\"o\">.<\/span><span class=\"n\">path<\/span><span class=\"o\">.<\/span><span class=\"n\">insert<\/span><span class=\"p\">(<\/span><span class=\"mi\">0<\/span><span class=\"p\">,<\/span> <span class=\"n\">os<\/span><span class=\"o\">.<\/span><span class=\"n\">getcwd<\/span><span class=\"p\">())<\/span>\n    <span class=\"kn\">import<\/span> <span class=\"nn\">electricpy<\/span> <span class=\"k\">as<\/span> <span class=\"nn\">ep<\/span>\n\n<span class=\"kn\">import<\/span> <span class=\"nn\">requests<\/span>\n\n<span class=\"n\">response<\/span> <span class=\"o\">=<\/span> <span class=\"n\">requests<\/span><span class=\"o\">.<\/span><span class=\"n\">get<\/span><span class=\"p\">(<\/span><span class=\"sa\">f<\/span><span class=\"s2\">&quot;https:\/\/api.github.com\/repos\/<\/span><span class=\"si\">{<\/span><span class=\"n\">USERNAME<\/span><span class=\"si\">}<\/span><span class=\"s2\">\/<\/span><span class=\"si\">{<\/span><span class=\"n\">REPO<\/span><span class=\"si\">}<\/span><span class=\"s2\">\/releases\/latest&quot;<\/span><span class=\"p\">)<\/span>\n<span class=\"k\">try<\/span><span class=\"p\">:<\/span>\n    <span class=\"n\">latest<\/span> <span class=\"o\">=<\/span> <span class=\"n\">response<\/span><span class=\"o\">.<\/span><span class=\"n\">json<\/span><span class=\"p\">()[<\/span><span class=\"s2\">&quot;name&quot;<\/span><span class=\"p\">]<\/span>\n<span class=\"k\">except<\/span> <span class=\"ne\">Exception<\/span><span class=\"p\">:<\/span>\n    <span class=\"n\">latest<\/span> <span class=\"o\">=<\/span> <span class=\"s1\">&#39;0.0.0&#39;<\/span>\n\n<span class=\"c1\"># Verify Version is Newer<\/span>\n<span class=\"n\">version<\/span> <span class=\"o\">=<\/span> <span class=\"sa\">f<\/span><span class=\"s2\">&quot;v<\/span><span class=\"si\">{<\/span><span class=\"n\">ep<\/span><span class=\"o\">.<\/span><span class=\"n\">_version_<\/span><span class=\"si\">}<\/span><span class=\"s2\">&quot;<\/span>\n<span class=\"k\">if<\/span> <span class=\"n\">version<\/span> <span class=\"o\">&lt;=<\/span> <span class=\"n\">latest<\/span><span class=\"p\">:<\/span>\n    <span class=\"k\">raise<\/span> <span class=\"ne\">ValueError<\/span><span class=\"p\">(<\/span><span class=\"s2\">&quot;Module version is not newer than previous release!&quot;<\/span><span class=\"p\">)<\/span>\n<span class=\"k\">else<\/span><span class=\"p\">:<\/span>\n    <span class=\"nb\">print<\/span><span class=\"p\">(<\/span><span class=\"n\">version<\/span><span class=\"p\">)<\/span>\n<\/code><\/pre><\/div>\n\n<p>So, that script does a couple things for us. It polls GitHub for the latest release marked in the repo under my\nusername and project name. It then verifies that the version is valid and <em>new<\/em>.<\/p>\n<h3>How About that Action Definition?<\/h3>\n<p>So, we've covered one of the four pieces we need to accomplish. What's left? Well, we still need to build the\nPython package (but that's easy) and then create the release and push it to PyPI. Lucky for us, both of those\nremaining \"questions\" there's ready-made GitHub actions! So what does this whole thing look like?<\/p>\n<div class=\"highlight\"><pre><span><\/span><code><span class=\"n\">name<\/span><span class=\"o\">:<\/span><span class=\"w\"> <\/span><span class=\"n\">Release<\/span>\n<span class=\"n\">on<\/span><span class=\"o\">:<\/span>\n<span class=\"w\">  <\/span><span class=\"n\">push<\/span><span class=\"o\">:<\/span>\n<span class=\"w\">   <\/span><span class=\"n\">branches<\/span><span class=\"o\">:<\/span>\n<span class=\"w\">     <\/span><span class=\"o\">-<\/span><span class=\"w\"> <\/span><span class=\"n\">master<\/span>\n\n<span class=\"n\">jobs<\/span><span class=\"o\">:<\/span>\n<span class=\"w\">  <\/span><span class=\"n\">release<\/span><span class=\"o\">:<\/span>\n<span class=\"w\">    <\/span><span class=\"n\">runs<\/span><span class=\"o\">-<\/span><span class=\"n\">on<\/span><span class=\"o\">:<\/span><span class=\"w\"> <\/span><span class=\"n\">ubuntu<\/span><span class=\"o\">-<\/span><span class=\"n\">latest<\/span>\n<span class=\"w\">    <\/span><span class=\"n\">steps<\/span><span class=\"o\">:<\/span>\n<span class=\"w\">      <\/span><span class=\"o\">-<\/span><span class=\"w\"> <\/span><span class=\"n\">uses<\/span><span class=\"o\">:<\/span><span class=\"w\"> <\/span><span class=\"n\">actions<\/span><span class=\"o\">\/<\/span><span class=\"n\">checkout<\/span><span class=\"err\">@<\/span><span class=\"n\">v2<\/span>\n<span class=\"w\">      <\/span><span class=\"err\">#<\/span><span class=\"w\"> <\/span><span class=\"n\">https<\/span><span class=\"o\">:\/\/<\/span><span class=\"n\">github<\/span><span class=\"o\">.<\/span><span class=\"na\">com<\/span><span class=\"sr\">\/marketplace\/actions\/s<\/span><span class=\"n\">etup<\/span><span class=\"o\">-<\/span><span class=\"n\">python<\/span>\n<span class=\"w\">      <\/span><span class=\"err\">#<\/span><span class=\"w\"> <\/span><span class=\"o\">^--<\/span><span class=\"w\"> <\/span><span class=\"n\">This<\/span><span class=\"w\"> <\/span><span class=\"n\">gives<\/span><span class=\"w\"> <\/span><span class=\"n\">info<\/span><span class=\"w\"> <\/span><span class=\"n\">on<\/span><span class=\"w\"> <\/span><span class=\"n\">matrix<\/span><span class=\"w\"> <\/span><span class=\"n\">testing<\/span><span class=\"o\">.<\/span>\n<span class=\"w\">      <\/span><span class=\"o\">-<\/span><span class=\"w\"> <\/span><span class=\"n\">name<\/span><span class=\"o\">:<\/span><span class=\"w\"> <\/span><span class=\"n\">Install<\/span><span class=\"w\"> <\/span><span class=\"n\">Python<\/span>\n<span class=\"w\">        <\/span><span class=\"n\">uses<\/span><span class=\"o\">:<\/span><span class=\"w\"> <\/span><span class=\"n\">actions<\/span><span class=\"o\">\/<\/span><span class=\"n\">setup<\/span><span class=\"o\">-<\/span><span class=\"n\">python<\/span><span class=\"err\">@<\/span><span class=\"n\">v1<\/span>\n<span class=\"w\">        <\/span><span class=\"k\">with<\/span><span class=\"o\">:<\/span>\n<span class=\"w\">          <\/span><span class=\"n\">python<\/span><span class=\"o\">-<\/span><span class=\"n\">version<\/span><span class=\"o\">:<\/span><span class=\"w\"> <\/span><span class=\"s2\">&quot;3.10&quot;<\/span>\n<span class=\"w\">      <\/span><span class=\"o\">-<\/span><span class=\"w\"> <\/span><span class=\"n\">name<\/span><span class=\"o\">:<\/span><span class=\"w\"> <\/span><span class=\"n\">Identify<\/span><span class=\"w\"> <\/span><span class=\"n\">Version<\/span>\n<span class=\"w\">        <\/span><span class=\"n\">id<\/span><span class=\"o\">:<\/span><span class=\"w\"> <\/span><span class=\"n\">version<\/span>\n<span class=\"w\">        <\/span><span class=\"n\">run<\/span><span class=\"o\">:<\/span><span class=\"w\"> <\/span><span class=\"o\">|<\/span>\n<span class=\"w\">          <\/span><span class=\"n\">python<\/span><span class=\"w\"> <\/span><span class=\"o\">-<\/span><span class=\"n\">m<\/span><span class=\"w\"> <\/span><span class=\"n\">pip<\/span><span class=\"w\"> <\/span><span class=\"n\">install<\/span><span class=\"w\"> <\/span><span class=\"n\">requests<\/span><span class=\"w\"> <\/span><span class=\"n\">build<\/span><span class=\"w\"> <\/span><span class=\"o\">--<\/span><span class=\"n\">user<\/span>\n<span class=\"w\">          <\/span><span class=\"n\">python<\/span><span class=\"w\"> <\/span><span class=\"o\">-<\/span><span class=\"n\">m<\/span><span class=\"w\"> <\/span><span class=\"n\">pip<\/span><span class=\"w\"> <\/span><span class=\"n\">install<\/span><span class=\"w\"> <\/span><span class=\"o\">-<\/span><span class=\"n\">r<\/span><span class=\"w\"> <\/span><span class=\"n\">requirements<\/span><span class=\"o\">.<\/span><span class=\"na\">txt<\/span><span class=\"w\"> <\/span><span class=\"o\">--<\/span><span class=\"n\">user<\/span>\n<span class=\"w\">          <\/span><span class=\"n\">output<\/span><span class=\"o\">=<\/span><span class=\"n\">$<\/span><span class=\"o\">(<\/span><span class=\"n\">python<\/span><span class=\"w\"> <\/span><span class=\"n\">release<\/span><span class=\"o\">-<\/span><span class=\"n\">version<\/span><span class=\"o\">.<\/span><span class=\"na\">py<\/span><span class=\"o\">)<\/span>\n<span class=\"w\">          <\/span><span class=\"n\">echo<\/span><span class=\"w\"> <\/span><span class=\"s2\">&quot;::set-output name=version::$output&quot;<\/span>\n<span class=\"w\">      <\/span><span class=\"o\">-<\/span><span class=\"w\"> <\/span><span class=\"n\">name<\/span><span class=\"o\">:<\/span><span class=\"w\"> <\/span><span class=\"n\">Build<\/span><span class=\"w\"> <\/span><span class=\"n\">Artifacts<\/span>\n<span class=\"w\">        <\/span><span class=\"k\">if<\/span><span class=\"o\">:<\/span><span class=\"w\"> <\/span><span class=\"n\">success<\/span><span class=\"o\">()<\/span>\n<span class=\"w\">        <\/span><span class=\"n\">id<\/span><span class=\"o\">:<\/span><span class=\"w\"> <\/span><span class=\"n\">build<\/span>\n<span class=\"w\">        <\/span><span class=\"n\">run<\/span><span class=\"o\">:<\/span><span class=\"w\"> <\/span><span class=\"o\">|<\/span>\n<span class=\"w\">          <\/span><span class=\"n\">python<\/span><span class=\"w\"> <\/span><span class=\"o\">-<\/span><span class=\"n\">m<\/span><span class=\"w\"> <\/span><span class=\"n\">build<\/span><span class=\"w\"> <\/span><span class=\"o\">--<\/span><span class=\"n\">sdist<\/span><span class=\"w\"> <\/span><span class=\"o\">--<\/span><span class=\"n\">wheel<\/span><span class=\"w\"> <\/span><span class=\"o\">--<\/span><span class=\"n\">outdir<\/span><span class=\"w\"> <\/span><span class=\"n\">dist<\/span><span class=\"o\">\/<\/span>\n<span class=\"w\">      <\/span><span class=\"o\">-<\/span><span class=\"w\"> <\/span><span class=\"n\">name<\/span><span class=\"o\">:<\/span><span class=\"w\"> <\/span><span class=\"n\">Create<\/span><span class=\"w\"> <\/span><span class=\"n\">Release<\/span>\n<span class=\"w\">        <\/span><span class=\"n\">uses<\/span><span class=\"o\">:<\/span><span class=\"w\"> <\/span><span class=\"n\">ncipollo<\/span><span class=\"o\">\/<\/span><span class=\"n\">release<\/span><span class=\"o\">-<\/span><span class=\"n\">action<\/span><span class=\"err\">@<\/span><span class=\"n\">v1<\/span>\n<span class=\"w\">        <\/span><span class=\"k\">with<\/span><span class=\"o\">:<\/span>\n<span class=\"w\">          <\/span><span class=\"n\">tag<\/span><span class=\"o\">:<\/span><span class=\"w\"> <\/span><span class=\"n\">$<\/span><span class=\"o\">{{<\/span><span class=\"w\"> <\/span><span class=\"n\">steps<\/span><span class=\"o\">.<\/span><span class=\"na\">version<\/span><span class=\"o\">.<\/span><span class=\"na\">outputs<\/span><span class=\"o\">.<\/span><span class=\"na\">version<\/span><span class=\"w\"> <\/span><span class=\"o\">}}<\/span>\n<span class=\"w\">          <\/span><span class=\"n\">name<\/span><span class=\"o\">:<\/span><span class=\"w\"> <\/span><span class=\"n\">Release<\/span><span class=\"w\"> <\/span><span class=\"n\">$<\/span><span class=\"o\">{{<\/span><span class=\"w\"> <\/span><span class=\"n\">steps<\/span><span class=\"o\">.<\/span><span class=\"na\">version<\/span><span class=\"o\">.<\/span><span class=\"na\">outputs<\/span><span class=\"o\">.<\/span><span class=\"na\">version<\/span><span class=\"w\"> <\/span><span class=\"o\">}}<\/span>\n<span class=\"w\">          <\/span><span class=\"n\">body<\/span><span class=\"o\">:<\/span><span class=\"w\"> <\/span><span class=\"n\">$<\/span><span class=\"o\">{{<\/span><span class=\"w\"> <\/span><span class=\"n\">steps<\/span><span class=\"o\">.<\/span><span class=\"na\">tag_version<\/span><span class=\"o\">.<\/span><span class=\"na\">outputs<\/span><span class=\"o\">.<\/span><span class=\"na\">changelog<\/span><span class=\"w\"> <\/span><span class=\"o\">}}<\/span>\n<span class=\"w\">          <\/span><span class=\"n\">artifacts<\/span><span class=\"o\">:<\/span><span class=\"w\"> <\/span><span class=\"s2\">&quot;dist\/*&quot;<\/span>\n<span class=\"w\">      <\/span><span class=\"o\">-<\/span><span class=\"w\"> <\/span><span class=\"n\">name<\/span><span class=\"o\">:<\/span><span class=\"w\"> <\/span><span class=\"n\">Publish<\/span><span class=\"w\"> <\/span><span class=\"n\">distribution<\/span><span class=\"w\"> <\/span><span class=\"err\">\ud83d\udce6<\/span><span class=\"w\"> <\/span><span class=\"n\">to<\/span><span class=\"w\"> <\/span><span class=\"n\">PyPI<\/span>\n<span class=\"w\">        <\/span><span class=\"k\">if<\/span><span class=\"o\">:<\/span><span class=\"w\"> <\/span><span class=\"n\">success<\/span><span class=\"o\">()<\/span>\n<span class=\"w\">        <\/span><span class=\"n\">uses<\/span><span class=\"o\">:<\/span><span class=\"w\"> <\/span><span class=\"n\">pypa<\/span><span class=\"o\">\/<\/span><span class=\"n\">gh<\/span><span class=\"o\">-<\/span><span class=\"n\">action<\/span><span class=\"o\">-<\/span><span class=\"n\">pypi<\/span><span class=\"o\">-<\/span><span class=\"n\">publish<\/span><span class=\"err\">@<\/span><span class=\"n\">master<\/span>\n<span class=\"w\">        <\/span><span class=\"k\">with<\/span><span class=\"o\">:<\/span>\n<span class=\"w\">            <\/span><span class=\"n\">password<\/span><span class=\"o\">:<\/span><span class=\"w\"> <\/span><span class=\"n\">$<\/span><span class=\"o\">{{<\/span><span class=\"w\"> <\/span><span class=\"n\">secrets<\/span><span class=\"o\">.<\/span><span class=\"na\">PYPI_API_TOKEN<\/span><span class=\"w\"> <\/span><span class=\"o\">}}<\/span>\n<\/code><\/pre><\/div>\n\n<p>That big definition basically does a bunch of stuff for us; I'll break it out by each of the steps:<\/p>\n<ol>\n<li>Check out the source code.<\/li>\n<li>Install Python 3.10 - Because we kinda need that. Notice here that <code>3.10<\/code> is in double quotes as: \"3.10\".\nThat's because otherwise, the GitHub system might mistake it as 3.1... You know, because 3.10 is really just\na decimal number with an extra 0 at the end.<\/li>\n<li>Use the Python Script (from above) to figure out the version. But not before installing the required\npackages; both for the script, and for <em>ElectricPy<\/em>.<\/li>\n<li>Build the Artifacts - The things we want to keep. Namely the source-code distribution (<code>--sdist<\/code>) and the\nwheel file.<\/li>\n<li>Create the GitHub release. This will place the package on the GitHub repo's \"Releases\" page and add a new\ntag to the repository so it's easy to back-track the code.<\/li>\n<li>Finally, push those artifacts to PyPI so they're available for download and install with <code>pip<\/code>.<\/li>\n<\/ol>\n<h3>Wrapping Up<\/h3>\n<p>This might not be the biggest accomplishment, but it's a huge relief because it makes automating releases\nand pushing out updates MUCH easier. So, let's bring on the new features and updates!!!<\/p>","category":[{"@attributes":{"term":"Python"}},{"@attributes":{"term":"automation"}},{"@attributes":{"term":"python"}},{"@attributes":{"term":"github actions"}},{"@attributes":{"term":"pypi"}}]},{"title":"DJ Joe Playlister","link":{"@attributes":{"href":"https:\/\/blog.stanleysolutionsnw.com\/dj-joe-playlister.html","rel":"alternate"}},"published":"2021-10-16T12:03:00-07:00","updated":"2021-10-16T12:03:00-07:00","author":{"name":"Joe Stanley"},"id":"tag:blog.stanleysolutionsnw.com,2021-10-16:\/dj-joe-playlister.html","summary":"<p>I've been going a bit crazy with the web-app craze lately. Let me show you what I've been up to...<\/p>","content":"<p>I've been working up a fever on web-apps recently. Ones that I'm developing, ones that I'm deploying, and ones that\nI'm reviewing. That goes for both work and home. Gosh... I think I need a vacation. Maybe next lifetime.<\/p>\n<p>Recently, at home, my focus has been on several \"DJ Joe Services,\" things that I can utilize for my mobile DJ work and\nthat will help me make those processes easier. Remember, I'm lazy! I want to find the easiest way to do things. Right\nnow, I've got two apps deployed, and I'll be working on a third here pretty soon.<\/p>\n<p>The first app was an availability calendar. I'll have to write about it soon, since it was a fun project tying APIs,\nPython, and React.js all into one solution. However, this is about my second app. What I call a \"playlister,\" i.e.,\nsomething that can slurp the playlist information out of another file\/service\/etc., and provide it in a more consumable\nmanner. Right now, it's focus is on Spotify and Apple-Music, since those are the two prominent sources that are\nconsistent enough for me to work with.<\/p>\n<p><img src=\"https:\/\/blog.stanleysolutionsnw.com\/Screenshot_20211016_120952.png\" style=\"width: 100%;\" alt=\"DJ Joe Playlister\"><\/p>\n<h3>Inspiration<\/h3>\n<p>As a mobile DJ, I often am provided \"playlists\" in various forms: Word documents, text\nfiles, quickly-scribbled hand-written notes, Spotify playlists, and Apple Music playlists.<\/p>\n<p>It quickly became apparent for me, that I spent <em>way<\/em> more time working through these\nSpotify playlists and Apple Music playlists to get them into a form that was actually\nhelpful for me. In most cases, I could not simply copy\/paste the Spotify list(s) out so\nthat I could search for the songs of interest in my own library and then determine whether\nI'd need to aqcuire additional music. Thus... I came to the conclusion, I'd want a little\nassistance from my computer.<\/p>\n<h3>Stages of Development<\/h3>\n<p>I originally started with a simple Tkinter-app that used the <a href=\"https:\/\/spotipy.readthedocs.io\/en\/latest\/\"><code>spotipy<\/code><\/a>\npackage to pull playlist information into a simple plain-text file. It was helpful, but\nended up incurring a few additional challenges of its own. The largest of which being the\nfact I had to securly pass the API secrets around with the script itself. This became a\nreal burden, so I decided to enhance the system into a full-service mini web-app that\ncould be utilized for exactly this purpose. The web-app could run persistently on a server\nthat could hang on to those secrets and allow me to access the tool from anywhere.<\/p>\n<p>Thus, the <code>djjoeplaylister<\/code> was born.<\/p>\n<h3>Technical Details<\/h3>\n<p>This app is built on the shoulders of giants, so let me give credit to those where it's due!<\/p>\n<p><strong>Technology Specs<\/strong><\/p>\n<ul>\n<li>Language: Python 3<\/li>\n<li>Web Framework: <a href=\"https:\/\/fastapi.tiangolo.com\/\">FastAPI<\/a><\/li>\n<li>Web Listener\/ASGI Server: <a href=\"https:\/\/uvicorn.org\/\">Uvicorn<\/a><\/li>\n<li>Reverse Proxy: <a href=\"https:\/\/nginx.com\/\">Nginx<\/a><\/li>\n<li>Hosting Provider: <a href=\"https:\/\/linode.com\/\">Linode virtual hosting<\/a><\/li>\n<li>Operating System: Ubuntu server<\/li>\n<li>App Deployment Enviromnent: Dockerized Container<\/li>\n<\/ul>\n<p><strong>Python Packages Leveraged<\/strong><\/p>\n<ul>\n<li>Spotify Client: <a href=\"https:\/\/spotipy.readthedocs.io\/en\/latest\/\"><code>spotipy<\/code><\/a><\/li>\n<li>Apple Music Client: <a href=\"https:\/\/docs.python-requests.org\/en\/latest\/\"><code>requests<\/code><\/a><\/li>\n<li>HTML Table Generation: <a href=\"https:\/\/pandas.pydata.org\/\"><code>pandas<\/code><\/a><\/li>\n<\/ul>\n<p>Additionally, I'd like to provide a special thanks and shout-out to this gist that\nhelped me get up and running with consuming the Apple Music playlist without dealing\nwith Apple's crummy developer program ($99 dolars a year, just to access an API? No\nthank you!)\n<a href=\"https:\/\/gist.github.com\/aleclol\/ef9e87d0964f00975f82d5373a814447\">https:\/\/gist.github.com\/aleclol\/ef9e87d0964f00975f82d5373a814447<\/a><\/p>\n<hr>\n<p>That's it! My little DJ Playlister! Want to go see it? <a href=\"https:\/\/playlists.djjoeidaho.com\/\">Go Check it Out!<\/a> It's not\nanything too terribly special, and it's got plenty of room to grow, but it's a helpful little tool, and I think it\nshowcases the utility of the Python programming language.<\/p>\n<h5>I mean... just think about it.<\/h5>\n<p>I started with a simple little Tkinter script for which I had to lug secrets around all the time, and it was great! But\nit had some significant shortcomings. Python to the rescue though, a little refactoring, and throw in some HTML, CSS,\nand some more packages and I've got a full web-application. Still full Python, and it's fully-deployed! You can't do that\nwith a lot of other tools. Imagine if I had started with some Excel macro, or some bash script. It would've been very\ndifficult to scale those apps out to something that's actually useful in the context that I need.<\/p>\n<p><strong><em>Not with Python!<\/em><\/strong><\/p>\n<p>Preaching session over. Chat again soon, goodbye!<\/p>","category":[{"@attributes":{"term":"Python"}},{"@attributes":{"term":"spotify"}},{"@attributes":{"term":"apple-music"}},{"@attributes":{"term":"python"}},{"@attributes":{"term":"web-apps"}},{"@attributes":{"term":"dj"}},{"@attributes":{"term":"docker"}}]},{"title":"A Better Way to Integrate with VoiceMeeter?","link":{"@attributes":{"href":"https:\/\/blog.stanleysolutionsnw.com\/a-better-way-to-integrate-with-voicemeeter.html","rel":"alternate"}},"published":"2021-09-12T17:07:00-07:00","updated":"2021-09-15T17:38:00-07:00","author":{"name":"Joe Stanley"},"id":"tag:blog.stanleysolutionsnw.com,2021-09-12:\/a-better-way-to-integrate-with-voicemeeter.html","summary":"<p>Wait... What? There's an API for VoiceMeeter? And there's already a Python API for it? Sign me up!<\/p>","content":"<p>So, I finally sucked it up and bought an Intel NUC to run as my own mini audio-server. For what purpose,\nyou ask? Well, so that I can have an \"always on\" <a href=\"https:\/\/vb-audio.com\/Voicemeeter\/vban.htm\">VBAN<\/a> server\nwhere I can route audio through-out my house. From my desktop, my laptop, my mixer, my stereo.... All\nover!<\/p>\n<p>Now, I did run into a little trouble in the process. My little NUC is mounted nicely out of the way at my\ndesk in my study. It looks great, and runs great... buuuuut... There's one little problem.<\/p>\n<p>When I connect or disconnect over a Remote Desktop (RDP) connection it gets, shall we say, a little mixed\nup. In fact, the VoiceMeeter audio engine falls all over itself and gets tangled up. Now, to get around\nthis problem, I can restart the audio engine, or the software itself. So I started looking into how to\nautomate the kick-in-the-pants the software needed. Mind you, I'd done this before by using Python to find\nthe process ID that VoiceMeeter was associated with, and kill it, restarting a moment later. But that's\nboring and slow. So I did some Googling...<\/p>\n<p>Turns out, VoiceMeeter has an <a href=\"https:\/\/forum.vb-audio.com\/viewtopic.php?f=8&amp;t=346\">API<\/a>! FANTASTIC!<\/p>\n<p>Now, I realize that that's a C-API, and I'd much rather do my programming in Python. I don't really want\nto fuss with installing GCC on my little NUC. SO... I started investigating how to wrap the C-API with\nPython. It's something I'd never done before, but I figured it must be possible! Once more, I turned to\nthe internet wizards, and found a very <a href=\"https:\/\/stackoverflow.com\/a\/252473\/10406011\">nice little article on StackOverflow<\/a>\non how to wrap a C-level DLL with Python.<\/p>\n<p>I used that article and proved to myself that, YES, I can write Python code to hit the DLL. But, it\noccurred to me, that maybe somebody else had already done that work.<\/p>\n<p>Back to Google...<\/p>\n<p>EUREKA! Turns out that someone (<a href=\"https:\/\/github.com\/chvolkmann\">Christian Volkmann<\/a>, to be specific) had\nalready written a full API against the DLL. It's all in Python, and it's glorious! Here...\n<a href=\"https:\/\/github.com\/chvolkmann\/voicemeeter-remote-python\">Go take a look!<\/a><\/p>\n<p>So all that left for me was whacking out a little script to run in the background, monitor for new RDP\nconnections, and restart the audio engine when the connection state changed. Here's what that looked\nlike:<\/p>\n<div class=\"highlight\"><pre><span><\/span><code><span class=\"c1\"># vmeetermanager - an automated tool to keep VoiceMeeter running correctly.<\/span>\n<span class=\"c1\"># (c) 2021 - Stanley Solutions | Joe Stanley<\/span>\n\n<span class=\"c1\"># Imports<\/span>\n<span class=\"kn\">import<\/span> <span class=\"nn\">voicemeeter<\/span>\n<span class=\"kn\">import<\/span> <span class=\"nn\">subprocess<\/span>\n<span class=\"kn\">import<\/span> <span class=\"nn\">time<\/span>\n\n\n<span class=\"c1\"># Define function to determine rdp connection<\/span>\n<span class=\"k\">def<\/span> <span class=\"nf\">is_rdp_connected<\/span><span class=\"p\">():<\/span>\n    <span class=\"n\">args<\/span> <span class=\"o\">=<\/span> <span class=\"p\">[<\/span><span class=\"s2\">&quot;netstat&quot;<\/span><span class=\"p\">,<\/span> <span class=\"s1\">&#39;-n&#39;<\/span><span class=\"p\">,<\/span> <span class=\"s1\">&#39;|&#39;<\/span><span class=\"p\">,<\/span> <span class=\"s2\">&quot;find&quot;<\/span><span class=\"p\">,<\/span> <span class=\"s1\">&#39;&quot;:3389 &quot;&#39;<\/span><span class=\"p\">]<\/span>\n    <span class=\"n\">resp<\/span> <span class=\"o\">=<\/span> <span class=\"n\">subprocess<\/span><span class=\"o\">.<\/span><span class=\"n\">run<\/span><span class=\"p\">(<\/span><span class=\"s1\">&#39; &#39;<\/span><span class=\"o\">.<\/span><span class=\"n\">join<\/span><span class=\"p\">(<\/span><span class=\"n\">args<\/span><span class=\"p\">),<\/span> <span class=\"n\">shell<\/span><span class=\"o\">=<\/span><span class=\"kc\">True<\/span><span class=\"p\">,<\/span> <span class=\"n\">capture_output<\/span><span class=\"o\">=<\/span><span class=\"kc\">True<\/span><span class=\"p\">)<\/span>\n    <span class=\"k\">if<\/span> <span class=\"s2\">&quot;ESTABLISHED&quot;<\/span> <span class=\"ow\">in<\/span> <span class=\"n\">resp<\/span><span class=\"o\">.<\/span><span class=\"n\">stdout<\/span><span class=\"o\">.<\/span><span class=\"n\">decode<\/span><span class=\"p\">(<\/span><span class=\"s1\">&#39;utf-8&#39;<\/span><span class=\"p\">):<\/span>\n        <span class=\"k\">return<\/span> <span class=\"kc\">True<\/span>\n    <span class=\"k\">return<\/span> <span class=\"kc\">False<\/span> <span class=\"c1\"># Default<\/span>\n\n\n<span class=\"c1\"># Main Body<\/span>\n<span class=\"k\">if<\/span> <span class=\"vm\">__name__<\/span> <span class=\"o\">==<\/span> <span class=\"s2\">&quot;__main__&quot;<\/span><span class=\"p\">:<\/span>\n    <span class=\"n\">last_state<\/span> <span class=\"o\">=<\/span> <span class=\"kc\">False<\/span>\n    <span class=\"c1\"># Establish VoiceMeeter Connection<\/span>\n    <span class=\"k\">while<\/span> <span class=\"kc\">True<\/span><span class=\"p\">:<\/span>\n        <span class=\"k\">try<\/span><span class=\"p\">:<\/span>\n            <span class=\"k\">with<\/span> <span class=\"n\">voicemeeter<\/span><span class=\"o\">.<\/span><span class=\"n\">remote<\/span><span class=\"p\">(<\/span><span class=\"s2\">&quot;banana&quot;<\/span><span class=\"p\">)<\/span> <span class=\"k\">as<\/span> <span class=\"n\">vmr<\/span><span class=\"p\">:<\/span>\n                <span class=\"c1\"># Run Loop<\/span>\n                <span class=\"k\">while<\/span> <span class=\"kc\">True<\/span><span class=\"p\">:<\/span>\n                    <span class=\"c1\"># Determine Connection State<\/span>\n                    <span class=\"n\">connected<\/span> <span class=\"o\">=<\/span> <span class=\"n\">is_rdp_connected<\/span><span class=\"p\">()<\/span>\n                    <span class=\"n\">changed_state<\/span> <span class=\"o\">=<\/span> <span class=\"n\">connected<\/span> <span class=\"o\">!=<\/span> <span class=\"n\">last_state<\/span>\n                    <span class=\"n\">last_state<\/span> <span class=\"o\">=<\/span> <span class=\"n\">connected<\/span>\n                    <span class=\"c1\"># If the state has changed, restart audio engine<\/span>\n                    <span class=\"k\">if<\/span> <span class=\"n\">changed_state<\/span><span class=\"p\">:<\/span>\n                        <span class=\"nb\">print<\/span><span class=\"p\">(<\/span><span class=\"sa\">f<\/span><span class=\"s2\">&quot;RDP Connection State Changed to: CONNECTED=<\/span><span class=\"si\">{<\/span><span class=\"n\">connected<\/span><span class=\"si\">}<\/span><span class=\"s2\">&quot;<\/span><span class=\"p\">)<\/span>\n                        <span class=\"n\">time<\/span><span class=\"o\">.<\/span><span class=\"n\">sleep<\/span><span class=\"p\">(<\/span><span class=\"mf\">0.25<\/span><span class=\"p\">)<\/span>\n                        <span class=\"n\">vmr<\/span><span class=\"o\">.<\/span><span class=\"n\">restart<\/span><span class=\"p\">()<\/span>\n                    <span class=\"c1\"># Don&#39;t overburden the systems<\/span>\n                    <span class=\"n\">time<\/span><span class=\"o\">.<\/span><span class=\"n\">sleep<\/span><span class=\"p\">(<\/span><span class=\"mi\">1<\/span><span class=\"p\">)<\/span>\n        <span class=\"k\">except<\/span> <span class=\"ne\">Exception<\/span><span class=\"p\">:<\/span>\n            <span class=\"nb\">print<\/span><span class=\"p\">(<\/span><span class=\"s2\">&quot;VoiceMeeter Hasn&#39;t Started Yet...&quot;<\/span><span class=\"p\">)<\/span>\n            <span class=\"n\">time<\/span><span class=\"o\">.<\/span><span class=\"n\">sleep<\/span><span class=\"p\">(<\/span><span class=\"mi\">3<\/span><span class=\"p\">)<\/span>\n<\/code><\/pre><\/div>\n\n<h3>What Else Will Come?<\/h3>\n<p>Goodness, there's so many other things that I can do with this now. Imagine having a full web-based front\nend that I could use to control it! That would be pretty awesome, wouldn't it? I'll really have to do\nsome more exploring with this!<\/p>","category":[{"@attributes":{"term":"Python"}},{"@attributes":{"term":"audio"}},{"@attributes":{"term":"voicemeeter"}},{"@attributes":{"term":"api"}},{"@attributes":{"term":"sdk"}},{"@attributes":{"term":"mixer"}},{"@attributes":{"term":"python"}}]},{"title":"GitLab, Jenkins, Python, and the Raspberry Pi!","link":{"@attributes":{"href":"https:\/\/blog.stanleysolutionsnw.com\/gitlab-jenkins-and-the-rpi.html","rel":"alternate"}},"published":"2020-12-21T19:07:00-08:00","updated":"2021-01-04T21:07:00-08:00","author":{"name":"Joe Stanley"},"id":"tag:blog.stanleysolutionsnw.com,2020-12-21:\/gitlab-jenkins-and-the-rpi.html","summary":"<p class=\"first\">I'm finally getting around to setting up some CI\/CD systems for my self-hosted GitLab server... About Time!<\/p>\n<p class=\"last\">CI\/CD, Dev Ops, Pipelines, Workflows, Automated Deployment<\/p>\n","content":"<p>Think that's enough buzz words to catch the Google SEO engine's eye?<\/p>\n<p>Probably not, I know, but I'm not going to spend anymore time on it at the moment. See,\nI've got bigger items to tackle! Namely, getting Jenkins set up on a Raspberry Pi, as\nthe name of this article so implies.<\/p>\n<p>Good news for you; I've cut out the &quot;dirty-work&quot; through the magic of &quot;blog-posting.&quot;<\/p>\n<p>As part of the work I've been tackling for some of the other open source projects I'm\ndeveloping, I need to develop a local (on-premises) continuous integration solution to\neffectively slam my code with testing and verification. After all, what's great code\nwithout equally great tests? I need to have the system on-premises for a couple reasons;\nthe largest of which being the fact that I need access to custom hardware.<\/p>\n<blockquote>\nSo, why a Pi? A Pi 3-B no less?!<\/blockquote>\n<p>Well, that's quite simple; actually. It's the only spare computer I have at the moment.<\/p>\n<p>So now that I've thoroughly introduced you, to my reasoning, and the topic at hand;\nlet's get into it!<\/p>\n<div class=\"section\" id=\"installing-jenkins-on-the-pi\">\n<h2>Installing Jenkins on the Pi<\/h2>\n<p>I already have GitLab set up on an old x86 laptop running Ubuntu Server 20.04, so\nfor this article, I'm going to focus on setting up Jenkins on a Raspberry Pi, and\ngetting the basics of the workflow between Jenkins and GitLab running.<\/p>\n<ol class=\"arabic\">\n<li><p class=\"first\">Start with a fresh Pi (latest build of the RaspberryPiOS). I had a Pi sitting\naround with an older build of Raspbian, but that's several years old, and I\nreally just wanted to start fresh.<\/p>\n<\/li>\n<li><p class=\"first\">Update the Raspberry Pi. Well, in the spirit of starting fresh, might as well\nupdate the system!<\/p>\n<\/li>\n<li><p class=\"first\">Install Java with:<\/p>\n<div class=\"highlight\"><pre><span><\/span><span class=\"gp\">$<\/span>&gt;<span class=\"w\"> <\/span>sudo<span class=\"w\"> <\/span>apt-get<span class=\"w\"> <\/span>install<span class=\"w\"> <\/span>openjdk-11-jre\n<\/pre><\/div>\n<\/li>\n<li><p class=\"first\">Verify Java Version with:<\/p>\n<div class=\"highlight\"><pre><span><\/span><span class=\"gp\">$<\/span>&gt;<span class=\"w\"> <\/span>java<span class=\"w\"> <\/span>--version\n<span class=\"go\">openjdk 11.0.9.1 2020-11-04<\/span>\n<span class=\"go\">OpenJDK Runtime Environment (build 11.0.9.1+1-post-Raspbian-1deb10u2)<\/span>\n<span class=\"go\">OpenJDK Server VM (build 11.0.9.1+1-post-Raspbian-1deb10u2, mixed mode)<\/span>\n<\/pre><\/div>\n<\/li>\n<li><p class=\"first\">At this point, I took some time to get the Python system up to a state that\nwould be a bit more useful for me. So I installed <cite>pip3<\/cite>, and a number of\nPython packages. I suppose this could really be done at any point during this\nwhole process, but I felt like this was the most sensible time.<\/p>\n<\/li>\n<li><p class=\"first\">Download and add the Jenkins Key with:<\/p>\n<div class=\"highlight\"><pre><span><\/span><span class=\"gp\">$<\/span>&gt;<span class=\"w\"> <\/span>wget<span class=\"w\"> <\/span>-q<span class=\"w\"> <\/span>-O<span class=\"w\"> <\/span>-<span class=\"w\"> <\/span>https:\/\/pkg.jenkins.io\/debian\/jenkins.io.key<span class=\"w\"> <\/span><span class=\"p\">|<\/span><span class=\"w\"> <\/span>sudo<span class=\"w\"> <\/span>apt-key<span class=\"w\"> <\/span>add<span class=\"w\"> <\/span>-\n<\/pre><\/div>\n<\/li>\n<li><p class=\"first\">Open a new file:<\/p>\n<div class=\"highlight\"><pre><span><\/span><span class=\"gp\">$<\/span>&gt;<span class=\"w\"> <\/span>sudo<span class=\"w\"> <\/span>nano<span class=\"w\"> <\/span>\/etc\/apt\/sources.list.d\/jenkins.list\n<\/pre><\/div>\n<p>Then add the following line and save the file to add the Jenkins repository as\na source:<\/p>\n<div class=\"highlight\"><pre><span><\/span>deb https:\/\/pkg.jenkins.io\/debian binary\/\n<\/pre><\/div>\n<\/li>\n<li><p class=\"first\">Next, another good <cite>sudo apt-get update<\/cite> is in order, followed by\n<cite>sudo apt-get install jenkins<\/cite><\/p>\n<\/li>\n<li><p class=\"first\">Using the command listed below, you can grab the initial admin password to get\nstarted:<\/p>\n<div class=\"highlight\"><pre><span><\/span><span class=\"gp\">$<\/span>&gt;<span class=\"w\"> <\/span>sudo<span class=\"w\"> <\/span>cat<span class=\"w\"> <\/span>\/var\/lib\/jenkins\/secrets\/initialAdminPassword\n<\/pre><\/div>\n<\/li>\n<li><p class=\"first\">Now it's time to navigate to <cite>&lt;raspberry-pi-ip-address:8080<\/cite> and use that fancy\npassword to log in for the first time and start the setup wizard; or should I say\nbutler?<\/p>\n<\/li>\n<\/ol>\n<p>After the &quot;butler&quot; has completed, it's time to get started with setting up some CI\njobs.<\/p>\n<\/div>\n<div class=\"section\" id=\"preparing-a-simple-pytest-job-with-jenkins\">\n<h2>Preparing a Simple <cite>pytest<\/cite> Job with Jenkins<\/h2>\n<p>Now, I'll caution that I this portion doesn't cover any of the GitLab\/Jenkins\ninterfacing, maybe I'll get to writing that in another article... As part of the\nmaterial I'm skipping, I'm going to breeze right over the GitLab connection and\nrepository information. I'm going to focus, instead, on the build operations.<\/p>\n<ol class=\"arabic\">\n<li><p class=\"first\">With the new Jenkins server up and running, create a &quot;New Item,&quot; give it a\ndescriptive, memorable name, and set it as a &quot;Freestyle Project&quot;<\/p>\n<img alt=\"Create a new project in Jenkins for CI.\" src=\"https:\/\/blog.stanleysolutionsnw.com\/jenkins-new-config.png\" style=\"width: 800px;\" \/>\n<\/li>\n<li><p class=\"first\">After configuring the various other settings relevant to the project (repository,\nbuild-triggers, etc.) find the <em>&quot;Build&quot;<\/em> section and from the <em>&quot;Add build step&quot;<\/em>\nselect <em>&quot;Execute shell&quot;<\/em>.<\/p>\n<\/li>\n<li><p class=\"first\">In the new &quot;Command&quot; field of the &quot;Execute shell&quot; section, insert the commands\nnecessary to navigate to the appropriate subdirectory and run <cite>pytest<\/cite>. In my case,\nmy pytest &quot;test folder&quot; is located in the root directory, so I don't really need\nto change the working directory; I just go and run <cite>pytest<\/cite>. I do run a few other\ngeneric commands just to make sure that I've got a fair report of the build\nenvironment in case I need to go back and debug some things. So, here's a sample\nof what my configuration might look like.<\/p>\n<div class=\"highlight\"><pre><span><\/span><span class=\"go\">echo &quot;Current Directory&quot;<\/span>\n<span class=\"go\">pwd<\/span>\n<span class=\"go\">echo &quot;List Folder Structure&quot;<\/span>\n<span class=\"go\">ls -a .\/&lt;name-of-my-python-package-folder&gt;<\/span>\n<span class=\"go\">echo &quot;Run pytest&quot;<\/span>\n<span class=\"go\">pytest -v<\/span>\n<\/pre><\/div>\n<\/li>\n<\/ol>\n<\/div>\n<div class=\"section\" id=\"summary\">\n<h2>Summary<\/h2>\n<p>Well, that's a pretty rough intro into what I've been doing in Jenkins and GitLab.\nKinda rough, but I hope I'll be looking to add more in the near future.<\/p>\n<\/div>\n","category":[{"@attributes":{"term":"Python"}},{"@attributes":{"term":"python"}},{"@attributes":{"term":"jenkins"}},{"@attributes":{"term":"gitlab"}},{"@attributes":{"term":"raspberry pi"}},{"@attributes":{"term":"dev ops"}},{"@attributes":{"term":"git"}},{"@attributes":{"term":"ci\/cd"}}]},{"title":"pycev - A Python CEV Reader","link":{"@attributes":{"href":"https:\/\/blog.stanleysolutionsnw.com\/pycev-a-python-cev-reader.html","rel":"alternate"}},"published":"2020-12-12T10:59:00-08:00","updated":"2020-12-12T10:59:00-08:00","author":{"name":"Joe Stanley"},"id":"tag:blog.stanleysolutionsnw.com,2020-12-12:\/pycev-a-python-cev-reader.html","summary":"<p class=\"first last\">Another new project? Well, why not? This time, we'll be tackling reading CEV files from SEL in Python.<\/p>\n","content":"<img alt=\"Introducing: pycev!\" src=\"https:\/\/raw.githubusercontent.com\/engineerjoe440\/pycev\/main\/logo\/pycev.png\" style=\"width: 600px;\" \/>\n<p>Wait...<\/p>\n<p>Another new project?<\/p>\n<p>Yes. That's right. I'm starting another new project. But hey! There's a lot of framework\nthat needs to be introduced before I can start doing all the cool high-level stuff that we\nall want to see and use. I mean, by now you should understand that I'm all about getting\nthe framework right. If you're still not sure, go read my rant about getting the framework\nright... <a class=\"reference external\" href=\"https:\/\/blog.stanleysolutionsnw.com\/write-framework-once.html\">This is my rant on getting framework right.<\/a><\/p>\n<div class=\"section\" id=\"goal\">\n<h2>Goal:<\/h2>\n<p>So what's the plan here, anyway?<\/p>\n<p>Well, this project, <cite>pycev<\/cite> (I've almost considered that it should be pronounced &quot;pie-safe&quot;, but\nthat's not how it looks, so we'll let that stew a while longer) will be a package for reading and\ninterpreting SEL Compressed EVent records. They're a proprietary (but open) format in which SEL\nprotective relays collect event information and &quot;compress&quot; it into a format that's easily read\nby machines (computers).<\/p>\n<p>There's already a handful of projects out in the wild for reading COMTRADE records; which, if\nyou're unfamiliar are &quot;<em>Common Format for Transient Data Exchange<\/em>&quot; files, and are supported by\nmany SEL relays in addition to a much broader number of other vendor devices. Trouble is, not\neveryone uses COMTRADE, and comparatively, CEV files are a little simpler, and (in my opinion)\nmore straight-forward and robust. Perhaps the best Python project for reading COMTRADE files\nis <a class=\"reference external\" href=\"https:\/\/github.com\/dparrini\/python-comtrade\">Python Comtrade<\/a>. That project shows great maturity and value. It also sees regular updates\nand bugfixes as needed.<\/p>\n<p>Since it's such a well respected and mature project, I'd like to take it as inspiration for\n<cite>pycev<\/cite> and use it to help me realize the best API for the package so that the two libraries\ncould (potentially) be used interchangeably for various projects.<\/p>\n<\/div>\n<div class=\"section\" id=\"what-s-first\">\n<h2>What's First?<\/h2>\n<p>Well, I guess starting the package development is first!<\/p>\n<p>I've already carved out a repository, and I've got something of a skeleton package put together.\nI think the first step will be getting enough working that I can upload it to PyPI to reserve\nthe namespace. Then full development will need to come. There's a good handful of things that\nneed to be tackled:<\/p>\n<ul class=\"simple\">\n<li>Upload Project to PyPI<\/li>\n<li>Develop Core Functionality and Match API to that of &quot;Python-Comtrade&quot;<\/li>\n<li>Develop Automated Test Suite with Local Server and Various Existing CEV Files<\/li>\n<\/ul>\n<p>So that's the sort of roadmap I see before me. Now, the time-frame is still way up in the air;\nso who knows whe this all will <em>actually<\/em> happen. But here's hoping!<\/p>\n<p>If you're interested in checking in on the project, and would like to jump in and contribute,\nhave a little look at the <a class=\"reference external\" href=\"https:\/\/github.com\/engineerjoe440\/pycev\">repository<\/a>, and feel free to open an <a class=\"reference external\" href=\"https:\/\/github.com\/engineerjoe440\/pycev\/issues\">issue<\/a> to start a conversation!<\/p>\n<\/div>\n","category":[{"@attributes":{"term":"Python"}},{"@attributes":{"term":"python"}},{"@attributes":{"term":"sel"}},{"@attributes":{"term":"cev"}},{"@attributes":{"term":"event"}},{"@attributes":{"term":"record"}},{"@attributes":{"term":"files"}},{"@attributes":{"term":"power system"}},{"@attributes":{"term":"analysis"}}]},{"title":"Reading Data with selprotopy","link":{"@attributes":{"href":"https:\/\/blog.stanleysolutionsnw.com\/reading-data-with-selprotopy.html","rel":"alternate"}},"published":"2020-11-22T19:02:00-08:00","updated":"2020-11-22T19:02:00-08:00","author":{"name":"Joe Stanley"},"id":"tag:blog.stanleysolutionsnw.com,2020-11-22:\/reading-data-with-selprotopy.html","summary":"<p class=\"first last\">Finally reading some data from SEL relays using Python! Now to get the controls working...<\/p>\n","content":"<p>Still deep in the process of getting a fully functional SEL protocol binding suite in Python, but,\nhey! At least I can write a little update on what's been going on!<\/p>\n<p>(if you haven't read my article on what <cite>selprotopy<\/cite> is, take a look <a class=\"reference external\" href=\"https:\/\/blog.stanleysolutionsnw.com\/sel-protocol-coming-to-python.html\">here<\/a>)<\/p>\n<p>In the past month of so, I've been able to really &quot;whack out&quot; some reasonable functionality. In\nfact, I've been able to poll an SEL-351 for both digital and analog data. For those of you who\nare a little familiar with SEL protocol, that means that I've been able to create a parser for\nthe relay definition block, and the various fast-meter blocks in addition to the DNA definition.\nTo boot, I've even tested (albeit breifly) on an SEL-751 and saw pretty promissing results.<\/p>\n<p>That's all pretty good, but the eventual goal (well, <em>my<\/em> eventual goal) is to be able to poll\nregularly and send commands\/controls as needed. I'd also like to be able to read CEV reports\n(more on that in the future) and perhaps the relay's SER (Sequential Event Recorder). So, is any\nof the control functionality working yet? Not really...<\/p>\n<p>I've gotten to the point where the commands <em>should<\/em> be configured and sent correctly to the\nrelay, but no dice.<\/p>\n<p>Somewhere along the lines, I've clearly &quot;bugged&quot; something up. So now, it's really just a matter\nof doing some additional debugging. Hmm... will need to get started on that. Trouble is, I've got\nlots of other fun projects to work on too!!!<\/p>\n<div class=\"section\" id=\"c-mon-joe-wrap-this-thing-up\">\n<h2>C'mon, Joe; wrap this thing up...<\/h2>\n<p>Okay, so I'm rambling; at this point, I'm pretty excited to say that I've got some polling working\nwith <cite>selprotopy<\/cite>, but there's clearly some more to work on. I'm hoping that I can get commands\nworking here pretty soon, and then I've got a handful of options as the next step.<\/p>\n<div class=\"section\" id=\"i-could\">\n<h3>I could:<\/h3>\n<ul class=\"simple\">\n<li>create a system to read the SER<\/li>\n<li>get the CEV reading figured out<\/li>\n<li>start testing on a variety of SEL relays (but this would require testing a FOSS project at SEL,\nso I'm still not sure about this one)<\/li>\n<li>get an automated test suite built on my local GitLab instance<\/li>\n<\/ul>\n<p>Clearly some more work coming, so stay tuned!<\/p>\n<\/div>\n<\/div>\n","category":[{"@attributes":{"term":"Python"}},{"@attributes":{"term":"protocols"}},{"@attributes":{"term":"sel"}},{"@attributes":{"term":"python"}},{"@attributes":{"term":"communications"}},{"@attributes":{"term":"metering"}}]},{"title":"Introducing selprotopy","link":{"@attributes":{"href":"https:\/\/blog.stanleysolutionsnw.com\/sel-protocol-coming-to-python.html","rel":"alternate"}},"published":"2020-09-20T11:07:00-07:00","updated":"2021-02-14T16:00:00-08:00","author":{"name":"Joe Stanley"},"id":"tag:blog.stanleysolutionsnw.com,2020-09-20:\/sel-protocol-coming-to-python.html","summary":"<p class=\"first last\">The SEL Protocol binding suite for Python is finally coming...<\/p>\n","content":"<p>SEL Protocol is finally coming for Python. That's right, you've heard correctly; it's coming.<\/p>\n<img alt=\"The new... `selprotopy`!\" src=\"https:\/\/raw.githubusercontent.com\/engineerjoe440\/sel-proto-py\/master\/logo\/selprotopy.png\" style=\"width: 350px;\" \/>\n<p>But, what is SEL Protocol anyway?<\/p>\n<p>Well, it's a communications protocol, or standard, that was developed by <a class=\"reference external\" href=\"https:\/\/selinc.com\/\">SEL<\/a> to support\nfast data communications between protective electric relays (the devices that monitor the\npower grid for faults) and communications processors. It was developed in the early '90s to\nhelp improve communication support of devices to allow users to monitor protective relays\nfrom a distance, and to perform control operations without being present.<\/p>\n<p>Anyway...<\/p>\n<p>I've begun writing <a class=\"reference external\" href=\"https:\/\/github.com\/engineerjoe440\/selprotopy\">selprotopy<\/a> which will be a protocol driver supporting SEL protocol in\nPython. This means that users will be able to integrate solutions with SEL relays using\nPython!<\/p>\n<p>Obviously, there's a lot to come, so this is not much more than an early announcement.\nIf you're interested in contributing, feel free to drop me a message! I'd love to interact!<\/p>\n","category":[{"@attributes":{"term":"Python"}},{"@attributes":{"term":"python"}},{"@attributes":{"term":"protocols"}},{"@attributes":{"term":"sel"}}]}]}