Skip to content

python

Preparing Silver Saucer for Deployment

Pyramid, my favorite Python web framework, was released version 2.0 late Sunday. The biggest change is dropping legacy Python support. I've been running the beta version of 2.0 the last couple of months while developing Silver Saucer.

The Pyramid 2.0 release helped me change gears from my hardware project back to Silver Saucer. Under the mantra of “release early, release often” I decided to start prepping to deploy Silver Saucer. I knew I had a few things to work on still, especially the big “On This Day” feature I haven’t started.

Of course I didn’t save any notes from when I last deployed a Pyramid web app. I started to make a list of things I needed to do on my DigitalOcean droplet and realized that Silver Saucer’s IP on my server wasn’t set up correctly which was why I kept getting an SSL error. That was an easy fix when I remember Dependabot was still broken on Github.

I had tried adding a pre-commit hook to use the black linter to my Github repository. It required me to create a pyproject.toml file. But doing this caused Github’s Dependabot to think I was using Poetry to manage my Python modules, when I’m not. I didn’t have any luck on the Github forums, so I just removed the file from now. Dependabot started working again right away - spewing out over two dozen pull requests to go through. It took Azure Pipelines a little while to catch up in testing out all the builds with the PRs, but only one or two failed and I still have one more PR to test and / or fix.

With that done, I thought I’d fix just one or two little things. This led me down a path of refactoring the theme, especially how the header works. I had the header image included in every page, and I was able to put it in just the layout template.

I also have a weird CSS bug with the viewport where it’s not filling the website screen 100% vertically. I spent hours on that but my CSS skills aren’t there yet, especially when using a complex CSS theme created by someone else.

I’m going to push Silver Saucer live in the next few days. The irony of creating a random Discogs player when I just started listening to my entire collection alphabetically is not lost on me. Release early, release often!

Always Learning

I haven’t blogged about my progress on Silver Saucer over the last month, but that doesn't mean I haven't been up to anything. I’ve continued to take the HTML, CSS, and Javascript on Coursera. I find CSS... difficult. Unfortunately, I also received an email from the course instructor that Coursera will be removing this course soon, so that figures. But it's motivation to get it done.

One of the things I tackled a few weeks ago was making an applet to sit in the macOS menu bar to control the volume of my Denon receiver located in my home theater. The receiver’s Zone 2 controls the volume in my office, where I’m usually listening to records. It took 30 minutes to throw some code together using RUMPS to make the applet and the denonavr Python module to control the receiver.

That led me down a path of looking into making a hardware project to make a dial to control the volume. Adafruit has a great tutorial on building one using Circuit Python, but I don’t have a 3D printer (yet).

That led me into looking more into the denonavr Python module - which is also the same module that powers Home Assistant. I’ve now reinstalled Home Assistant - it’s been a couple of years since I last tried it out. Now I can just turn to a browser page I leave pinned and control my receiver from there. I’ve spent a few days playing and tweaking Home Assistant to see most of my “smart” devices, but I’ve never been all that into having a smart home (nor will you find any voice assistants here). Of course this has given me a dozen ideas of things to build or integrate. But I think I still want to build a dial!

Denon Control in Home Assistant

I also spent a few days doing a deep dive into Docker. I have a couple containers that I run on my NAS, but never really understood the right way to update or manage them. Now that I have a little more knowledge I realize how slick containers really are.

Finally, I was on vacation last week and started to tackle another project I’ve talked about with a friend for the last couple years. We need a pump (or pumps) programmed for a project. I had some old Raspberry Pi computers laying around and a year ago we quickly prototyped a solution. Unfortunately, we needed a more powerful pump - both in how it pumps and in power consumption.

I’ve been learning about circuits and electricity by watching a lot of YouTube videos and reading a number of books I’ve picked up over the years (especially on Humble Bundle) . I then went out and bought bench power supply unit (PSU) and multimeter for my workbench. I figured out how to power the breadboard with a 3.3v / 5v breadboard power supply first and then figured out how to use the PSU, too. It was kind of exciting to see a little blue LED light up the first time I got a circuit right:

Well, that’s exciting. Sometimes it’s the little things. pic.twitter.com/QM89RgzhjU

— Paul Cutler (@prcutler) February 12, 2021

Here’s a picture of my messy workbench below with the Pi hooked up to a breakout board which connects to the breadboard. Also connected to the breadboard is a DRV8825 motor controller which is connected to a NEMA step-motor.

In theory, I have everything hooked up correctly now to start prototyping. Now the hard part starts: Is the code wrong or the hardware hooked up incorrectly?!

My workbench

Silver Saucer Update

Now that I have the ability to pick a random record to play using the Discogs API, I’ve completed one of the two big features.

I’m now debating what to do next. I have a few options, all still focused on the random album feature. (I may have to cancel the “On This Day” future, but that’s a different discussion for a different time).

Learn CSS

I need to learn CSS as the theme I’m using needs to be updated. The text colors in the theme are too dark and I had to use a hack to show the text in white. This also means that the link colors are also appearing as white and you can’t tell that they are links. In the past I’ve been able to get away with just poking at a CSS file to get done what I want, but I really should take the time to learn it. The CSS file for Silver Saucer is huge and there are parts of it when I look at it that I have no idea what it does.

Bootstrap

I think I need to learn CSS before I tackle Bootstrap. Right now when the album results are returned, everything is centered on the page. I played around with trying to justify it left and right and it doesn’t work at all visually. I need to create a Bootstrap container to put the text in so I can justify it correctly.

Release Date

The Release Date returned is when that particular version of an album was released. If it’s a re-release or repressing, that date can be very different from when the album was originally released. I think I can use the master release number to go get a list of all releases and then look at the first release in that list to get the “real” release date. It doesn’t always work in some random testing I did, but it’s a start. My idea to look at the Musicbrainz API doesn’t look like it’s any better. The other challenge with this feature is working with datetime objects, which can be frustrating. Especially considering Discogs might return the release date in multiple different formats, including just the month, month and year, or date / month / year. That will be fun to standardize.

Miscellaneous

I also need to fix the data returned in Genre. It’s a list that I currently show as a list of bullets, each on its own line. It doesn’t look quite right and I need to probably update the CSS or when iterating over the list, put it on one line. I haven’t quite figured out what to do with it yet.

I also, in theory, could deploy what I have now. But I think I'll wait just a bit.

Next Steps

I’m leaning towards tackling CSS first. I've started the Responsive Web Design class on Free Code Camp, and it's good content, but the way they have you practice I don't feel as if the information is sticking in my head. I’ve also signed up for a free (to audit) five week course on Coursera to learn HTML, CSS, and Javascript. Part of me really wants to learn just a little Javascript, but most of me doesn’t - I’d rather focus on getting better at Python as I don’t feel like I’m anywhere close from transition from novice Python to intermediate Python. But if I’m going to continue to focus on using Python for web development, which is where my passion seems to lie at the moment, I need to get better at HTML and CSS. The structure of an actual class is helpful, so I’ll probably move forward with the Coursera class for the time being.

Play Singles Part 2

I was talking to my wife yesterday about my progress and she agreed that I should update my existing method used for albums to also work for singles - and that I shouldn’t just re-use that method and to avoid code duplication. It’s good to get confirmation that I’m on the right path.

But I’m stuck on how to do this.

If we look at the route to play a full album:

@view_config(route_name="play", renderer="silversaucer:templates/play/play.pt")
def play(_):

    album_release_id = RandomRecordService.get_folder_count(2162484)
    release_data = RandomRecordService.get_album_data(album_release_id)
    return {"release_info": release_data}

Here I’m manually passing the folder ID for my Discogs Folder that contains full albums (2162484). That ID is passed to a method that gets a count of how many items are in that folder to pick one at random. Once that happens, it passes the release ID of the pick to get the album’s information.

Works great for full albums. As mentioned in my last blog post, it bombs if it’s a single, because there’s no concept of a main (or “master”) release associated with EPs and singles.

So how do I refactor my code to account for this?

I started by updating the route to play a full album, adding a folder parameter to the get_folder_count method instead of passing the folder number manually:

@view_config(route_name="play", renderer="silversaucer:templates/play/play.pt")
def play(_):

    folder = 2162484
    album_release_id = RandomRecordService.get_folder_count(folder)
    release_data = RandomRecordService.get_album_data(folder, album_release_id)
    return {"release_info": release_data}

And I tell that method to now return both the release ID and the folder:

def get_folder_count(folder):

    discogs_api = folder_url + "?=" + api_token

    # TODO Add an if statement to check for a 200 or 404 response code and redirect on 404 to error page

    response = requests.get(discogs_api)

    record_json = response.json()
      return random_album_release_id, folder

Keeping with my goal of only using one method and passing the folder to it, I added an if statement to check if it’s the folder with full albums. If it is, get the data I know is working. If not, don’t get the data that’s not included in an EP or single.

@staticmethod
def get_album_data(folder, album_release_id):

    if folder == 2162484:

        release_api = (
            config.discogs_url
            + "releases/"
            + str(album_release_id)
            + "?"
            + config.discogs_user_token
        )
        print(release_api)
        response = requests.get(release_api)
        print(response)

But now my method to get the release information on an album broke, when it was working before:

  File "/Users/prcutler/workspace/silversaucer/silversaucer/controllers/music_controller.py", line 14, in play
    release_data = RandomRecordService.get_album_data(folder, album_release_id)
  File "/Users/prcutler/workspace/silversaucer/silversaucer/services/play_service.py", line 114, in get_album_data
    release_uri = release_json["uri"]
KeyError: 'uri'

Digging into it with another print statement, I see that the value for album_release_id is now returning the folder ID correctly, but `album_release_id is returning a tuple:

Folder ID = 2162486 Album = (3200538, 2162486) <class 'tuple'>

Which is why the API call below is screwed up:
File "/Users/prcutler/workspace/silversaucer/silversaucer/services/play_service.py", line 114, in get_album_data release_uri = release_json["uri"] KeyError: 'uri' https://api.discogs.com/releases/(1366279, 2198941)?&token=
But it’s a tuple!  I can work with that. The `folder` number is always returned as the second part of the tuple and
the `album_release_id` is always the first index. So let’s update the `release_api` first to use the first index:
release_api = ( config.discogs_url + "releases/" + str(album_release_id[0]) + "?" + config.discogs_user_token )

And now everything works! I updated the else statement to go get the data I need and not include the values that were returning a KeyError and now with one method - that includes an if / else statement, I can get back the release information for any record in my collection, no matter what folder it’s in. If and when I add other media - especially CDs or tapes that have a main release, I’ll need to update the if statement, but future me can deal with that. I don’t even have that stuff cataloged yet. (But I made sure to add a TODO to my code so I don’t forget.

And here’s a screenshot of a random 10” EP that was returned. Yay!

The Decemberists - Long LIve the King

Wiring Up Play Singles

Earlier this year when I finished cataloging all my records in Discogs, I then put each one into a “folder”, a way for Discogs to separate different kinds of records. It’s setup by the user, so I created a folder for each type of music I have, and the number is the ID that Discogs assigned to each one:

all_folder = 0
lp_folder = 2162484
twelve_inch_folder = 2198941
ten_inch_folder = 2162486
seven_inch_folder = 2162483
cd_folder = 2162488
tape_folder = 2162487
digital_folder = 2198943

CD, Digital and Tape don’t currently have any items in them, but I hope to at least catalog my CD collection some day, but that’s a different story.

In the music controller, the method to get an album’s information looks like this:

@view_config(route_name="play", renderer="silversaucer:templates/play/play.pt")
def play(_):

    album_release_id = RandomRecordService.get_folder_count(2162484)
    release_data = RandomRecordService.get_album_data(album_release_id)
    return {"release_info": release_data}

Here I’ve hard coded the folder ID (2162484) from Discogs in the method as I only want to use the folder that has only full albums (no singles, 45s, etc.).

Now it’s time to wire up the code to play a single from one of three folders: * 7” or 45 * 10” EPs * 12” singles / remixes

It looks similar to the method above, but I’ve added a random method to pick one of the three folders for me and pass the folder ID to the play service that I’ve already written for full albums:

def play_single(_):

    random_folder = randint(0, 2)
    if random_folder == 0:
        single = 2162483
    elif random_folder == 1:
        single = 2162486
    else:
        single = 2198941

    album_release_id = RandomRecordService.get_folder_count(single)
    print(album_release_id)
    release_data = RandomRecordService.get_album_data(album_release_id)
    print(release_data)

    return {"release_info": release_data}

This is where I appear to have outsmarted myself. I thought I had blogged about this, but apparently not. At one point I had refactored the first play method above - I had added the play service to accept the folder ID and pass that. I was pretty proud of myself because after the refactoring, I figured this would work for exactly what I’m trying to do now - pass the folder ID of other folders. But no, it wasn’t meant to be as it errors out:

  File "/Users/prcutler/workspace/silversaucer/silversaucer/controllers/music_controller.py", line 35, in play_single
    release_data = RandomRecordService.get_album_data(album_release_id)
  File "/Users/prcutler/workspace/silversaucer/silversaucer/services/play_service.py", line 178, in get_album_data
    discogs_main_id = release_json["master_id"]
KeyError: 'master_id'

There is no master_id returned because singles don’t have a master release! Only full albums have a master release to track all the regional releases and special pressings.

I need to think through this and decide if I’m going to write a new method in play_service to handle getting back the release information for the single or if I should write an if statement to account for the Key Error and return a different dictionary without the master_id.

I was so proud of myself for doing something I thought was Pythonic, too!

Displaying the Album Information

In my last blog post, I was able to pass the folder to Discogs to get a list of albums, and then pick one at random and give me information back about the album picked. That information was returned as a dictionary and now it is time to wire it up to the Chameleon template in Pyramid that will display the HTML.

Here's an example of the JSON:

{'release_title': 'Massey Fucking Hall', 'release_uri': 'https://www.discogs.com/Japandroids-Massey-Fucking-Hall/release/16118824', 'artist_name': 'Japandroids', 'artist_url': 'https://api.discogs.com/artists/1473872', 'release_date': '2020-06-26', 'discogs_main_id': 1829048, 'discogs_main_url': 'https://api.discogs.com/masters/1829048', 'main_release_date': 2020, 'release_image_uri': 'https://img.discogs.com/BVmRNi_A5_Vm8X0ntzUDh8figvE=/fit-in/300x300/filters:strip_icc():format(jpeg):mode_rgb():quality(90)/discogs-images/R-16118824-1604177632-7503.jpeg.jpg', 'genres': ['Rock']}
In the controller that handles the routing for the page I have:

```@view_config(route_name="play", renderer="silversaucer:templates/play/play.pt") def play(_):

album_release_id = RandomRecordService.get_folder_count(2162484)
print(album_release_id)
release_data = RandomRecordService.get_album_data(album_release_id)
print(release_data)

return {"release_info": release_data}

It was way easier than expected. (And if you know me, you know that the random record picked above is perfect, as I'm a huge Japandroids fan.) The first thing I did was connect the image URI that the Discogs API passes in the Chameleon template: ${release_info.release_title} That worked, which was awesome. The whole image is shown, and I don't have to worry about parsing any of the URL. I quickly added the values to show the *Artist*, *Album*, and *Release Date* information:

Artist: ${release_info.artist_name}

Album: ${release_info.release_title}

Released: ${release_info.release_date}

\

It looks like this:

![Japandroids - Massey Fucking Hall](japandroids.png)

Looking at Debbie Gibson's *Electric Youth*, the *Genre* key in the dictionary returns a list:
'genres': ['Electronic', 'Pop']
With some trial and error I was able to display the list, but it showed up as code, looking like:
`['Electronic', 'Pop']`.  No one wants to see that.  The challenge is that Chameleon templates use something like a 
shorthand for Python code.  I revisited two of my former projects where I would iterate over a list and show the 
results, but after lots of trial and error I felt like I was going backwards.  Some more search engine queries and 
Stack Overflow searching I did what I always do when I get stuck:  I asked my wife.  It took her about 15 minutes to 
figure out how to loop over the list and show it in bullets:

  • ``` I was so close - she pointed out the mistake I made with having two `tal:repeat` methods in the template. That’s what’s hard about the trial and error method of finding the solution, especially as I wasn’t writing down the methods that kind of worked. (Oops. Usually I have a note going in Bear to capture things like this, but sometimes you get in the zone and just keep trying things as you get closer. Documentation is good!) Lastly, I changed the image size of the album image returned from Discogs, shrinking it from 400x400 to 300x300 and it seems to fit in the Bootstrap container better. You can see the difference with Debbie Gibson's *Electric Youth* below at 400x400 and Japandroids at 300x300 above. ![Debbie Gibson - Electric Youth](debbie-gibson.png) Now I have lots of cleanup to do. I need to turn the text into links, for example if you click on the artist, Japandroids, it takes you to their page on Discogs. I also need to fix how it justifies and draws the bullets. I think almost all of this is done in CSS and I don't know CSS at all... (I had to cheat to make the text color white to show up on the page already.) Random thoughts and musings: * Every time the page loads, it refreshes the API call. So if you click a link to the artist page on Discogs, for example, and then click the back button, that album data has been replaced. Not sure how to work around that, though I have some ideas. Probably a good question in IRC with the Pyramid team. * I like Chameleon templates. Not only is it the first template language I've learned, I've looked at Jinja templates and they look harder to use. They're used much more widely and that seems to lead to better documentation, but I don't have plans to change from Chameleon. * I had more thoughts, but I'm finishing up this blog post almost a week later as my internet went out. * The *Release Date* returned from Discogs has the same problem I've already talked about for the future "On This Day" feature I want to add. It could be just the year or the full release date, you never know what you're going to get. Look at the two screenshots above! I don't know if I want to add all the functionality to return the "correct" release date yet. I'm worried about how long the API calls take to load the page. (Even if it's just for me and I know how long it takes, I don't want it taking long.)
  • Iterating Through the Folder Dictionary

    After taking a break and coming back to it, I finally was able to iterate through the dictionary containing the folders I’ve sorted my records in to on Discogs.

    Dictionaries are a basic object in Python, but my struggles show that to be good - at anything - takes practice. And I haven’t been practicing. Thank goodness for Stack Overflow!

    I embedded all kinds of print statements trying to figure out how to play with the JSON from Discogs.

    After lots of trial and error, Google searches and Stack Overflow searches, I’ve got it.

    After making the API call to Discogs, we get the JSON back:

    record_json = response.json()
    
    json_data = record_json
    json_folders = json_data[”folders”]
    

    json_folders is a list, so we add the last line above. Now I had to make a for loop to iterate through that list - which is a list of dictionaries.

    The if statement looks to see if folder - which we passed from the controller when the user clicked which kind of album they want a random answer to, matches the folder ID found in the JSON.

    for get_folder_id in json_folders:
        print(json_folders)
        print(len(json_folders))
    
        if get_folder_id[”id”] == folder:
    
            lp_count = get_folder_id[”count”]
            print(”Folder passed = “, folder, ”LP Count is :”, lp_count)
            # folder_test = 2162484
            print(folder, lp_count)
    

    Voila! From there, I get a random number between one and the count found in the JSON. That number is then passed to the pagination request to get the album ID.

    Next up: Either fix the pagination method to reduce the if / else statement or move on to getting the album release information. I also need to remove a ton of print statements and clean up and remove all the methods I don’t have to use after refactoring this. Progress!

    Silver Saucer Random Play Progress

    I’ve made some good progress over the last few days on being able to pick an album at random out of a given folder and get information about it.

    All of the work has focused on the back-end. Once I know I’m getting the right data out of Discogs, I’ll hook up the front-end to display it, but for now, this has been a little more complex than I expected. (It always is, isn’t it?)

    One thing that’s jumped out at me was I don’t understand how people do TDD (Test Driven Development). The amount of trial and error I do, even on “simple” things like building the right URL for the API call is ridiculous. I know it’s because I’m a novice at Python and I don’t do this for a living all day every day, but I have a lot of respect for those that do TDD. One of my original goals was to do this in TDD, but I have my excuses.

    You can tell I’m a novice by how procedural my code is. Do this, then do that. It was working well in a very procedural manner, so yesterday I refactored it all. Yup, I ripped it up and started over. Well, not from scratch. I could visualize in my head how to make the code more Pythonic and I was able to re-use some of the code, but most of it was refactored in the play_service. A good example is going back to the folder example. I had hard coded each folder ID and each folder had it’s own method to get a random album release ID and its data.

    I’ve been able to take the folder selected by the user (LP, EP or single) and just pass variable that to the method in play_service.py. This allowed me to use the same method to get the release ID information and the method doesn’t care if it’s a single or full album.

    I still need to fix the loop I’m iterating through in the JSON, but I’m really close. This is one of the downsides to being a hobbyist - so much I don’t remember, including basic things, like how to iterate through a dictionary.

    Practice makes perfect and I needs lots of practice. I told myself I need to think of some more projects to work on, but one project at a time. In the meantime, I should sign up for something like Python Morsels and keep up with it.

    This weekend I’m hoping to fix looping over the list in the dictionary and also fix API call that deals with the pagination of results from Discogs. You can see what a mess it is in the play_service.py file and I know there has to be a better way to programmatically determine the pagination than a long if / else statement. (And if I ever get over a 1000 records it will break! Or my wife will break me when I have that many…)

    Discogs Authentication

    I was ready this morning to write a new blog post chronicling all the problems I had yesterday trying to authenticate against the Discogs API. After finding the motivation to sit down and code for a few hours, I ended the day with almost nothing done. I could access the parts of my collection that were public, but the parts that required authentication I couldn’t figure out.

    I sat down with my copy of coffee this morning, re-read the Discogs API authentication documentation, and voila! It just clicked. Sometimes you just need a good night’s sleep.

    You have two options for authentication when building an app with the Discogs API: full Oauth that allows users to login and validate their login, or pass a user token as part of the API query. Since my app is just going to use my Discogs information, the user token is perfect. Both authentication options give you options for a higher rate limit and the ability to return an image - and this last one will be important later.

    I thought I would tackle what would be the easiest part first - have the app pick a random album for me to play.

    It was a lot harder than I expected, but I’ve made good progress.

    Now that I could get back details of my collection via authentication, I had Discogs return a list of all the folders I’ve organized my music into. I put each of the following into its own folder, as when I hit the random button, I don’t want it to pick a 45 or 7” record with only two songs on it. I have 10 folders, with 0 and 9 included by default:

    • 0 - all
    • 1 - 10” (10” records, mostly EPs)
    • 2 - 12” (12” singles or EPs)
    • 3 - 7” (7” or 45 rpm singles)
    • 4 - Autographed (currently empty, but you can have a release in more than one folder)
    • 5 - Cassette
    • 6 - CD
    • 7 - Digital
    • 8 - LP
    • 9 - Uncategorized

    The JSON returned looks like this:

    {
        "folders": [
            {
                "id": 0,
                "name": "All",
                "count": 799,
                "resource_url": "https://api.discogs.com/users/prcutler/collection/folders/0"
            },
            {
                "id": 2162486,
                "name": "10\"",
                "count": 19,
                "resource_url": "https://api.discogs.com/users/prcutler/collection/folders/2162486"
            },
            {
                "id": 2198941,
                "name": "12\"",
                "count": 30,
                "resource_url": "https://api.discogs.com/users/prcutler/collection/folders/2198941"
            },
            {
                "id": 2162483,
                "name": "7\"",
                "count": 76,
                "resource_url": "https://api.discogs.com/users/prcutler/collection/folders/2162483"
            },
            {
                "id": 2162485,
                "name": "Autographed",
                "count": 0,
                "resource_url": "https://api.discogs.com/users/prcutler/collection/folders/2162485"
            },
            {
                "id": 2162487,
                "name": "Cassette",
                "count": 1,
                "resource_url": "https://api.discogs.com/users/prcutler/collection/folders/2162487"
            },
            {
                "id": 2162488,
                "name": "CD",
                "count": 6,
                "resource_url": "https://api.discogs.com/users/prcutler/collection/folders/2162488"
            },
            {
                "id": 2198943,
                "name": "Digital",
                "count": 1,
                "resource_url": "https://api.discogs.com/users/prcutler/collection/folders/2198943"
            },
            {
                "id": 2162484,
                "name": "LP",
                "count": 666,
                "resource_url": "https://api.discogs.com/users/prcutler/collection/folders/2162484"
            },
            {
                "id": 1,
                "name": "Uncategorized",
                "count": 0,
                "resource_url": "https://api.discogs.com/users/prcutler/collection/folders/1"
            }
        ]
    }
    

    I then got stuck for a while before I realized I should be using the folder IDs in the resource_url and not the JSON number of 0-9.

    That - plus authentication - are challenges for someone like me still learning. But progress is being made. Lots of trial and error with string concatenation to get the URLs and tokens right. The next thing to solve for: Discogs only returns 100 items in a list, making you interact with pagination to go deeper. I’ll need to do this to take the random number generated of while album to play to get its data, including the image to show, when a random album is chosen.

    Pre-Commit Git Hooks

    It was a pleasant surprise recently to get an email from Michael Kennedy at Talk Python. As a previous buyer of one of the Talk Python Everything Bundle which includes a number of different Python training courses, I was eligible for the “Pro” Talk Python podcast subscription, which doesn’t included ads.

    Going through the backlog, I added a bunch of episodes, and the timing of Episode 282: Pre-Commit Framework was perfect. Just as I’m getting back into working on my projects, now is a perfect time to set up Black to help format my code. The documentation was easy to follow as well as other walk throughs on the web, and now every time I check in code, it runs black to format it Pythonically, isort to automatically sort my imports in each of my files, and I’ll be adding flake8 this weekend, too.

    Thanks again for the Pro podcast subscription, Talk Python!