# Tyrel's Blog

## Rotate a Matrix in Python

I've been doing Advent of Code for a few years now, and every year I do it in my favorite language, Python. One thing that comes up a lot, is rotating matrices.

One way to do this, is to use Numpy, using np.rot90(mat), but not everyone wants to install Numpy just to do one small task. I know I don't always.

The way I always do it, that will support non-square matrixes, is to use zip.

```>>> matrix = [
[1,2,3],
[4,5,6],
[7,8,9]
]
>>> rotated = list(zip(*matrix[::-1]))
# And result is
[[7, 4, 1],
[8, 5, 2],
[9, 6, 3]]
```

We can break this down bit by bit.

This will copy the list, with a -1 step, resulting in a reverse order

```>>> matrix[::-1]
[[7,8,9],
[4,5,6],
[1,2,3]]
```

Next we need to call zip in order to get the x-th item from each inner list, but first, we need to unpack it. If you'll notice, the unpacked version isn't wrapped with another list, which is what zip needs from us.

```# Too many lists
>>> print(matrix[::-1])
[[7, 8, 9], [4, 5, 6], [1, 2, 3]]

# Just right
>>> print(*matrix[::-1])
[7, 8, 9] [4, 5, 6] [1, 2, 3]
```

From there, we can pass this unpacked list of - in our case - three lists, to zip (and in Python 3 this returns a generator, so we need to call list again on it, or just use it)

```>>> # Again, we get the rotated matrix
>>> list(zip(*matrix[::-1]))
[[7, 4, 1],
[8, 5, 2],
[9, 6, 3]]
```

## Notes

Small note: If you run this, you will actually get a list of tuples, so you can map those back to a list, if you need to update them for any reason. I just wanted square brackets in my examples.

```# This is just messy looking, so I didn't mention it until now
>>> list(map(list, zip(*matrix[::-1])))
```

As I mentioned, due to using zip this will work with non-square examples as well.

```>>> matrix = [
... [1,2,3,4,5,6,7,8,9],
... [9,8,7,6,5,4,3,2,1],
... ]
>>> print(list(zip(*matrix[::-1])))
[(9, 1),
(8, 2),
(7, 3),
(6, 4),
(5, 5),
(4, 6),
(3, 7),
(2, 8),
(1, 9)]
```
· · ·  python

#### Dec 16, 2022

Advent of Code this year is kicking my butt so I haven't been doing any tech blogging really lately. If you want to follow my progress, I think I might be done as of day 15 - This one seems to be a traveling salesman/knapsack problem related. Here's my repo: https://gitea.tyrel.dev/tyrel/advent-of-code/src/branch/main/2022/python.

I'm not on the computer that runs it, but I've been spending a lot of time playing with Apple's System7 in the BasiliskII emulator. Might have some fun projects with that coming up, but wanted to do some more learning before I start anything. So I have been going through a course on 6052 Assembly programming for the NES, and I'm about 73% done with that, it's really great!

It's By Gustavo Pezzi at Pikuma, if "oldschool" programming floats your boat then I definitely recommend it. It's all programming through making roms with CC65/CA65 assembler, and using FCEUX to see your results, super neat.

I've been picking up some more Go work at work. My current team is sort of disbanding so I'm going to be moving away from doing just Python. It's been a year since I've done Go stuff, since I left Tidelift, so I'm really rusty.

Speaking of Rust, I was trying to do Advent of code in Rust also, and made it TWO whole days in Rust. It's still on my bucket of stuff to learn, but my free time seems to be running out lately, and I have a lot of things on my plate to get done.

## NOTES

This post is ported over from my wiki, so the format isn't as storytelling as a blog post could be, but I wanted it here.

## Home Assistant Parts

### Zoom Plugin

I followed the Read Me from https://github.com/raman325/ha-zoom-automation#installation-single-account-monitoring and set up a Zoom Plugin for my account, that will detect if I am in a meeting or not.

### Pi Zero

I have a tiny project Enclosure box that I dremeled a hole for the GPIO pins in the cover and I then sandwich the Blinkt onto the Pi Zero with another dremeled hole running to the micro usb power, and that's it for hardware.

For software, I installed the python packages for Pimoroni and Blinkt, which came with a lovely set of sample projects. I deleted everything except the mqtt.py file, which I then put my Mosquitto server settings.

I then added a new service in systemd to control the mqtt server

```[Unit]
Description=Meeting Indicator

[Service]
Type=simple
ExecStart=/usr/bin/python2 /home/pi/mqtt.py
Restart=always
RestartSec=2

[Install]
WantedBy=sysinit.target
```

Pleased with the results, and testing by sending some messages over mqtt that changed the color, I then dove into Node-RED

### Node-Red

This is my first project using Node-RED, so I'm sure I could optimize better, but I have two entry points, one is from running HomeAssistant app on my mac, which gets me sensor data for my webcam, and the other is the aforementioned Zoom Presence plugin I created. These are Events:State nodes.

When either of these are True, they call first my ceiling light to turn on, which next will then add a msg.payload of

```rgb,0,255,0,0
rgb,1,255,0,0
rgb,2,255,0,0
rgb,3,255,0,0
rgb,4,255,0,0
rgb,5,255,0,0
rgb,6,255,0,0
rgb,7,255,0,0
```

as one string. This leads to a Split, which will in turn, emit a new MQTT message for each line (I split on \n) and turn on all 8 LEDs as red. This is inefficient because I am still using the sample code for the blinkt which requires you to address each LED individually, my next phase I will remove the pin requirement and just have it send a color for all of them at once, one line.

When either of the sensors states are False, I then flow into a Time Range node, in which I check if it's between 9-5 or not. If it is, then I turn all the LEDs Green, and if it's outside 9-5 I just turn the LEDs off. I do not turn OFF the overhead light, in case it was already on. I don't care about the state enough.

I also intentionally trigger at the Office Hours node, which will inherently turn the Green on at 9:01am, and off at 5:01pm. As well as turn on Red for any long standing meeting times I have.

### Source

Nodered configuration source json https://gist.github.com/tyrelsouza/c94329280848f0319d380cc750e995c2

## Django ORM - My History

I'm not the best SQL developer, I know it's one of my weak points. My history is I did php/mysql from the early 2000s until college. In college I didn't really focus on the Database courses, the class selection didn't have many database course. The one Data Warehousing course I had available, I missed out on because I was in England doing a study abroad program that semester. My first job out of college was a Python/Django company - and that directed my next eight years of work.

Django, if you are unaware, is a MVC framework that ships with a really great ORM. You can do about 95% of your database queries automatically by using the ORM.

```entry, created = Entry.objects.get_or_create(headline="blah blah blah")
```
```q = Entry.objects.filter(headline__startswith="What")
q = q.filter(pub_date__lte=datetime.date.today())
q = q.exclude(body_text__icontains="food")
```

Above are some samples from the DjangoDocs. But enough about Django.

## My Requirements

Recently at my job I was given a little bit of leeway on a project. My team is sort of dissolving and merging in with another team who already does Go. My Go history is building a CLI tool for the two last years of my previous job. I had never directly interacted with a database from Go yet. I wanted to spin up a REST API (I chose Go+Gin for that based on forty five seconds of Googling) and talk to a database.

## GORM

Being that I come from the Django (and a few years of ActiveRecord) land, I reached immediately for an ORM, I chose GORM. If you want to skip directly to the source, check out https://gitea.tyrel.dev/tyrel/go-webservice-gin. Full design disclosure: I followed a couple of blog posts in order to develop this, so it is in the form explictly decided upon by the logrocket blog post and may not be the most efficient way to organize the module.

In order to instantiate a model definition, it's pretty easy. What I did is make a new package called models and inside made a file for my Album.

```type Album struct {
ID     string  `json:"id" gorm:"primary_key"`
Title  string  `json:"title"`
Artist string  `json:"artist"`
Price  float64 `json:"price"`
}
```

This tracks with how I would do the same for any other kind of struct in Go, so this wasn't too difficult to do. What was kind of annoying was that I had to also make some structs for Creating the album and Updating the Album, this felt like duplicated effort that might have been better served with some composition.

I would have structured the controllers differently, but that may be a Gin thing and how it takes points to functions, vs pointers to receivers on a struct. Not specific to GORM. Each of the controller functions were bound to a gin.Context pointer, rather than receivers on an AlbumController struct.

The FindAlbum controller was simple:

```func FindAlbum(c *gin.Context) {
var album models.Album
if err := models.DB.Where("id = ?", c.Param("id")).First(&album).Error; err != nil {
}
c.JSON(http.StatusOK, gin.H{"data": album})
}
```

Which will take in a /:id path parameter, and the GORM part of this is the third line there.

```models.DB.Where("id = ?", c.Param("id")).First(&album).Error
```

To run a select, you chain a Where on the DB (which is the connection here) and it will build up your query. If you want to do joins, this is where you would chain .Joins etc... You then pass in your album variable to bind the result to the struct, and if there's no errors, you continue on with the bound variable. Error handling is standard Go logic, if err != nil etc and then pass that into your API of choice (Gin here) error handler.

This was really easy to set up, and if you want to get a slice back you just use DB.Find instead, and bind to a slice of those structs.

```var albums []models.Album
models.DB.Find(&albums)
```

## SQLX

SQLX is a bit different, as it's not an ORM, it's extensions in Go to query with SQL, but still a good pattern for abstracting away your SQL to some dark corner of the app and not inline everywhere. For this I didn't follow someone's blog post — I had a grasp on how to use Gin pretty okay by now and essentially copied someone elses repo with my existing model. gin-sqlx-crud.

This one set up a bit wider of a structure, with deeper nested packages. Inside my internal folder there's controllers, forms, models/sql, and server. I'll only bother describing the models package here, as thats the SQLX part of it.

In the models/album.go file, there's your standard struct here, but this time its bound to db not json, I didn't look too deep yet but I presume that also forces the columns to set the json name.

```type Album struct {
ID     int64   `db:"id"`
Title  string  `db:"title"`
Artist string  `db:"artist"`
Price  float64 `db:"price"`
}
```

An interface to make a service, and a receiver are made for applying the CreateAlbum form (in another package) which sets the form name and json name in it.

```func (a *Album) ApplyForm(form *forms.CreateAlbum) {
a.ID = *form.ID
a.Title = *form.Title
a.Artist = *form.Artist
a.Price = *form.Price
}
```

So there's the receiver action I wanted at least!

Nested inside the models/sql/album.go file and package, is all of the Receiver code for the service. I'll just comment the smallest one, as that gets my point across. Here is where the main part of GORM/SQLX differ - raw SQL shows up.

```func (s *AlbumService) GetAll() (*[]models2.Album, error) {
q := `SELECT * FROM albums;`

var output []models2.Album
err := s.conn.Select(&output, q)
// Replace the SQL error with our own error type.
if err == sql.ErrNoRows {
return nil, models2.ErrNotFound
} else if err != nil {
return nil, err
} else {
return &output, nil
}
}
```

This will return a slice of Albums - but if you notice on the second line, you have to write your own queries. A little bit more in control of how things happen, with a SELECT * ... vs the gorm DB.Find style.

To me this feels more like using pymysql, in fact its a very similar process. (SEE NOTE BELOW) You use the service.connection.Get and pass in what you want the output bound to, the string query, and any parameters. This feels kind of backwards to me - I'd much rather have the order be: query, bound, parameters, but thats what they decided for their order.

## Conclusion

Overall, both were pretty easy to set up for one model. Given the choice I would look at who the source code is written for. If you're someone who knows a lot of SQL, then SQLX is fine. If you like abstractions, and more of a "Code as Query" style, then GORM is probably the best of these two options.

I will point out that GORM does more than just "query and insert" there is migration, logging, locking, dry run mode, and more. If you want to have a full fledged system, that might be a little heavy, then GORM is the right choice.

SQLX is great if what you care about is marshalling, and a very quick integration into any existing codebase.

## Notes

I sent this blog post to my friend Andrey and he mentioned that I was incorrect with my comparision of sqlx to pymysql. To put it in a python metaphor, "sqlx is like using urllib3, gorm is like using something that generates a bunch of requests code for you. Using pymysql is like using tcp to do a REST request." Sqlx is more akin to SqlAlchemy core vs using SqlAlchemy orm. Sqlx is just some slight extensions over database/sql. As the sort of equivalent to pymysql in Go is database/sql/driver from the stdlib.

## New Blog - Pelican!

If you have read the previous post, and then looked at this one, there are a LOT of changes that happened. I was recently exploited and had heysrv.php files everywhere, so I have decided to forego wordpress for now. I am now using Pelican!

It's very sleek, and only took me a few hours to port my Wordpress export to Pelican reStructuredText format.

All I have to do is run invoke publish and it will be on the server. No PHP, no database. All files properly in their right places.

It comes with your standard blogging experience: Categories, Tags, RSS/Atom feeds, etc. You need to set up Disqus — which I probably won't — in order to get comments though.

I'm pleased with it. I have posts go under YYYY/MM/slug.html files, which I like for organization. Posting images is easy, I just toss it under content/images/YYYY/MM/ with date for organization.

· · ·  python  pelican

## Parsing Epubs

Recently I've become frustrated with the experience of reading books on my Kindle Paperwhite. The swipe features, really bother me. I really like MoonReader on Android, but reading on my phone isn't always pleasing. This lead me to look into other hardware. I've been eyeing the BOOX company a while ago, but definitely considering some of their new offerings some time. Until the time I can afford the money to splurge on a new ebook reader, I've decided to start a new project, making my own ebook reader tools!

I'm starting with EPUBs, as this is one of the easiest to work with. At its core, an EPUB is a zip file with the .epub extension instead of .epub with many individual XHTML file chapters inside it. You can read more of how they're structured yourself over at FILEFORMAT.

The tool I've chosen for reading EPUBs is the Python library ebooklib. This seemed to be a nice lightweight library for reading EPUBs. I also used DearPyGUI for showing this to the screen, because I figured why not, I like GUI libraries.

My first task was to find an EPUB file, so I downloaded one from my calibre server. I convert all my ebook files to .epub and .mobi on my calibre server so I can access them anywhere I can read my OPDS feed. I chose Throne of Glass (abbreviating to TOG.epub for rest of post). Loading I launched Python, and ran

```>>> from ebooklib import epub
```

This returned me a <ebooklib.epub.EpubBook object...> , seeing I had an EpubBook I ran a dir(book) and found the properties available to me

```['add_author', 'add_item', 'add_metadata', 'add_prefix',
'bindings', 'direction', 'get_item_with_href', 'get_item_with_id',
'get_items', 'get_items_of_media_type', 'get_items_of_type',
'items', 'language', 'metadata', 'namespaces', 'pages', 'prefixes',
'reset', 'set_cover', 'set_direction', 'set_identifier', 'set_language',
'templates', 'title', 'toc', 'uid', 'version']
```

Of note, the get_item_with_X entries caught my eye, as well as spine. For my file, book.spine looks like it gave me a bunch of tuples of ID and a "yes" string of which I had no Idea what was. I then noticed I had a toc property, assuming that was a Table of Contents, I printed that out and saw a bunch of epub.Link objects. This looks like something I could use.

I will note, at this time I was thinking that this wasn't the direction I wanted to take this project. I really wanted to learn how to parse these things myself, unzip, parse XML, or HTML, etc., but I realized I needed to see someone else's work to even know what is going on. With this "defeat for the evening" admitted, I figured hey, why not at least make SOMETHING, right?" I decided to carry on.

Seeing I was on at least some track, I opened up PyCharm and made a new Project. First I setup a class called Epub, made a couple of functions for setting things up and ended up with

```class Epub:
def __init__(self, book_path: str) -> None:
self.title: str = self.contents.title
```

I then setup a parse_chapters file, where I loop through the TOC. Here I went to the definition of Link and saw I was able to get a href and a title, I decided my object for chapters would be a dictionary (I'll move to a DataClass later) with title and content. I remembered from earlier I had a get_item_by_href so I stored the itext from the TOC's href: self.contents.get_item_with_href(link.href).get_content(). This would later prove to be a bad decision when I opened "The Fold.epub" and realized that a TOC could have a tuple of Section and Link, not just Links. I ended up storing the item itself, and doing a double loop in the parse_chapters function to loop if it's a tuple.

```def parse_chapters(self) -> None:
idx = 0
for _item in self.toc:
if isinstance(_item, tuple):  # In case is section tuple(section, [link, ...])
idx += 1
else:
idx += 1
```

_parse_link simply makes that dictionary of title and item I mentioned earlier, with a new index as I introduced buttons in the DearPyGUI at this time as well.

```def _parse_link(self, idx, link) -> None:
self.chapters.append(dict(
index=idx,
title=title,
))
```

That's really all there is to make an MVP of an EPUB parser. You can use BeautifulSoup to parse the HTML from the get_body_contents() calls on items, to make more readable text if you want, but depending on your front end, the HTML may be what you want.

In my implementation my Epub class keeps track of the currently selected chapter, so this loads from all chapters and sets the current_text variable.

```def load_view(self) -> None:
item = self.chapters[self.current_index]['item']
soup = BeautifulSoup(item.get_body_content(), "html.parser")
text = [para.get_text() for para in soup.find_all("p")]
self.current_text = "\n".join(text)
```

I don't believe any of this code will be useful to anyone outside of my research for now, but it's my first step into writing an EPUB parser myself.

The DearPyGUI steps are out of scope of this blog post, but here is my final ebook Reader which is super inefficient!

I figure the Dedication page is not as copywrited as the rest of the book, so it's fair play showing that much. Sarah J Maas, if you have any issues, I can find another book for my screenshots.

· · ·  epub  python

## Finished my GitHub CLI tool

I never intended this to be a full fleshed CLI tool comparable to the likes of the real GitHub CLI. This was simply a way to refresh myself and have fun. I have accomplished this, and am now calling this "Feature Complete". You can play around with it yourself from the repository on gitlab.

## TESTING

With that accomplished, I then added pytest-cov to my requirements.in and was able to leverage some coverage checks. I was about 30% with the latest changes (much higher than anticipated!) so I knew what I wanted to focus on next. The API seemed the easiest to test first again, so I changed around how I loaded my fixtures and made it pass in a name and open that file instead. In real code I would not have the function in both my test files, I would refactor it, but again, this is just a refresher, I'm lazy.

I decided earlier that I also wanted to catch HTTP 403 errors as I ran into a rate limit issue. Which, I assure you dear reader, was a thousand percent intentional so I would know what happens. Yeah, we'll go with that.

Py.Test has a context manager called pytest.raises and I was able to just with pytest.raises(httpx.HttpStatusError) and check that raise really easily.

The next bits of testing for the API were around the pagination, I faked two responses and needed to update my link header, checking the cases where there was NO link, was multiple pages, and with my shortcut return - in case the response was an object not a list. Pretty straight forward.

The GHub file tests were kind of annoying, I'm leveraging rich.table.Table so I haven't been able to find a nice "this will make a string for you" without just using rich's print function. I decided the easiest check was to see if the Table.Columns.Cells matched what I wanted, which felt a little off but it's fine.

The way I generated the table is by making a generator in a pretty ugly way and having a bunch of repo['column'], repo['column'] responses, rather than doing a dict comprehension and narrowing the keys down. If I ever come back to this, I MIGHT reassess that with a {k:v for k,v in repos if k in SELECTED_KEYS} and then yield a dictionary, but it's not worth the effort.

Overall I'd say this project was fun. It gave me a glimpse back into the Python world, and an excuse to write a couple blog posts. My next project is to get a Django site up and running again, so I can figure out how to debug my django-dbfilestorage.

## Closing Thoughts

If I had to do this again, I would probably have tried some test driven development. I've tried in the past, but I don't work on a lot of greenfield projects. I tend to be the kind of engineer who jumps HEAD FIRST into code and then tests are an after thought.

I also kind of want to rewrite this in Go and Rust, two other languages I've been fond of lately, just to see how they'd compare in fun. I haven't done any API calls with Rust yet, only made a little Roguelike by following Herbert Wolverson's Hands-On-Rust book. The Tidelift CLI is all Go and a bazillion API calls (okay like ten) so that wouldn't be too hard to use like SPF13's Cobra CLI library and make a quick tool that way.

One fun thing I learned while moving things over to GitLab is that my user Tyrel is a super early adopter. I was in the first 36,000 people! I showed a screenshot of my user ID to my friend Sunanda at GitLab and we had fun finding that out.

· · ·  python  cli

## Python3 GitHub CLI tool as a refresher

It's no lie that I love terminals. I wish I could live on a terminal and never really need to see a GUI application again.

Last night I migrated a lot of my old code from one GitLab account to another (tyrelsouza to tyrel) in an effort to clean up some of my usernames spread across the world. While doing that I noticed my django-dbfilestorage Python module that has been sitting and rotting for three years. I played around a little bit in order to port it to Python 3.9, but I ran into some base64 errors. I tried a little bit but it was late and I couldn't figure it out. My resolve is that I have been away from Python for too long so the little things - that I knew and love - had fallen away. I mentioned this to my friend Alex and he said "make a barebones github cli (readonly?) with issue viewer, and stats display". I've embarked on a journey to refresh my Python well enough to repair DBFS.

I knew I wanted to use httpx as my network client library, it's new, fast, and I have a couple friends who work on it. I started with a barebones requirements.in file, tossed in invoke, pytest, and black. From there I used pip-compile to generate my requirements.txt - (a tip I picked up recently while adding Pip-Compile support to the Tidelift CLI) and I was good to go.

The docs for the GitHub API are pretty easy to read, so I knew all I really needed to do was set my Accept header to be Version3 and I could view the schema. With the schema saved to a .json file I then wrote a GHub class to pull this data down using httpx.client.Client.get, super simple! The only two endpoints I care about right now are the user and repos endpoints, so I made two get_ functions for each. After a little bit of work - which I won't bore you with the super intricate details - I have a functional cli.py file. For now, the only interaction is a propmt from rich for a username, and then you get a fancy table (also from rich) of the first page of results of repos, stargazer/watchers/forks counts, and a description.

It was a fun evening of learning what's changed in Python3 since I last touched it, especially as I've spent the majority of my career in Python2.7. Type annotations are super awesome. I'll probably pick it up again once I get some more free time later in the week. It's also nice blog fodder! I already have a million things I want to do next - pagination, caching, some more interaction.

I know the tool I'm writing is nothing special, especially with their own cli now, but I'm not looking at reinventing the wheel!

Check out the code so far on my GitLab (heh, ironic it's there).

Dependencies: httpx, pip-tools, black, invoke, pytest, pytest-httpx, rich.

· · ·  python  cli

## Too many open files

When I worked at Propel Marketing, we used to outsource static websites to a third party vendor, and then host them on our server. It was our job as developers to pull down the finished website zip file from the vendor, check it to make sure they used the proper domain name, (they didn't a lot of the time,) and make sure it actually looks nice. If these few criteria were met, we could launch the site.

Part of this process was SCPing the directory to our sites server. The sites server was where we had Apache running with every custom static site as a vhost. We would put the website in /var/www/vhosts/domain.name.here/ and then create the proper files in sites-available and sites-enabled (more on this in another entry). After that the next step was to run a checkconfig and restart Apache.

Here's where it all went wrong one day. If I can recall correctly, my boss was on vacation so he had me doing a bit of extra work and launching a few more sites than I usually do. Not only that, but we also had a deadline of the end of the month which was either the next day, or the day after. I figure I'll just setup all mine for two days, and then have some extra time the next day for other things to work on. So I started launching my sites. After each one, I would add the domain it was supposed to be at into my /etc/hosts file and make sure it worked.

I was probably half way done with my sites, and suddenly I ran into one that didn't work. I checked another one to see if maybe it was just my network being stupid and not liking my hosts file, but no, that wasn't the problem. Suddenly, EVERY SITE stopped working on this server. Panicking, I delete the symlink in sites-enabled and restart Apache. Everything works again. I then proceed to put that site aside, maybe something in the php files breaks the server, who knows, but I have other sites I can launch.

I setup the next site and the same thing happens again, no sites work. Okay, now it's time to freak out and call our sysadmin. He didn't answer his phone, so I call my boss JB. I tell him the problem and he says he will reach out to the sysadmin and see what the problem is, all the while I'm telling JB "It's not broken broken, it just doesn't work, it's not my fault" etc etc. A couple hours later, our sysadmin emails us back and says he was able to fix the problem.

It turns out, there's a hard limit to the number of files your system can have open per user, and this was set to 1000 for the www-data user. The site I launched was coincidentally the 500th site on that server, each of them having an access.log and an error.log. These two files apparently constantly open on each site for apache to log to. He was able to change www-data's ulimit to a lot higher, (I don't recall now what it was) and that gave a lot more leeway in how many sites the sites server could host.

## Python Debugger

When I worked at Propel Marketing, my dev team used to have presentations on things they loved. I love the Python debugger. It's very useful and I believe a proper understanding of how to use a debugger, will make you a better programmer. Here is a presentation on the debugger I made for my team. https://prezi.com/cdc4uyn4ghih/python-debugger/

· · ·  python  pdb
Next → Page 1 of 2