rust-ssg/blog/posts/2021-11-05_finished-my-github-cli-tool.rst
2023-10-14 14:03:36 -04:00

62 lines
4.8 KiB
ReStructuredText

Finished my GitHub CLI tool
###########################
:date: 2021-11-05 00:08
:author: tyrel
:category: Tech
:tags: python, cli
:slug: finished-my-github-cli-tool
:status: published
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 <https://gitlab.com/Tyrel/ghub>`__.
HTTPX.LINKS
-----------
Today I learned some fun things with ``httpx`` mainly. The main thing I focused on today was figuring out pagination. The GitHub API uses the ``link`` header which I had never seen before.
The format of the header is a url, and then a relationship of that url. Lets take my friend Andrey's repos for example:
.. raw:: html
<figure class="wp-block-pullquote">
..
``{'link': '<https://api.github.com/user/6292/repos?page=2>; rel="next", <https://api.github.com/user/6292/repos?page=7>; rel="last"', ...}``
sample header value from Getting Andrey's repositories list and checking pagination
.. raw:: html
</figure>
The link header there has two items split by a comma, each with two fields split by a semicolon, the first of which is a URL inside angle brackets... and AGHH this is going to be annoying to parse! Luckily ``httpx`` responses have this handled and ``client.get(...).links`` returns a lovely dictionary of the proper data.
With a ``response.links.get('next')`` check, you can get the url from ``response.links['next']['url']``. So much nicer than writing some regular expressions.
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 <https://pragprog.com/titles/hwrust/hands-on-rust/>`__. The `Tidelift CLI <https://tidelift.com/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.