mirror of
https://github.com/jbranchaud/til
synced 2026-07-06 17:20:33 +00:00
Compare commits
14 Commits
03c11f9042
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| d4766dad95 | |||
| 5f4308c1af | |||
| 8c463a90e3 | |||
| 329267c7a2 | |||
| 12180acb02 | |||
| 8f722061b4 | |||
| a126b13d7c | |||
| 0ae3f14c25 | |||
| b91527db30 | |||
| c8f8c2c1a3 | |||
| c397e35ffd | |||
| bbb28fd811 | |||
| 950e2f861a | |||
| 20bbdb6d55 |
@@ -10,7 +10,7 @@ working across different projects via [VisualMode](https://www.visualmode.dev/).
|
|||||||
|
|
||||||
For a steady stream of TILs, [sign up for my newsletter](https://visualmode.kit.com/newsletter).
|
For a steady stream of TILs, [sign up for my newsletter](https://visualmode.kit.com/newsletter).
|
||||||
|
|
||||||
_1798 TILs and counting..._
|
_1810 TILs and counting..._
|
||||||
|
|
||||||
See some of the other learning resources I work on:
|
See some of the other learning resources I work on:
|
||||||
|
|
||||||
@@ -60,6 +60,7 @@ If you've learned something here, support my efforts writing daily TILs by
|
|||||||
* [Linux](#linux)
|
* [Linux](#linux)
|
||||||
* [LLM](#llm)
|
* [LLM](#llm)
|
||||||
* [Mac](#mac)
|
* [Mac](#mac)
|
||||||
|
* [Math](#math)
|
||||||
* [Mise](#mise)
|
* [Mise](#mise)
|
||||||
* [MongoDB](#mongodb)
|
* [MongoDB](#mongodb)
|
||||||
* [MySQL](#mysql)
|
* [MySQL](#mysql)
|
||||||
@@ -470,6 +471,7 @@ If you've learned something here, support my efforts writing daily TILs by
|
|||||||
- [List PRs Awaiting Your Review](github/list-prs-awaiting-your-review.md)
|
- [List PRs Awaiting Your Review](github/list-prs-awaiting-your-review.md)
|
||||||
- [Open A PR To An Unforked Repo](github/open-a-pr-to-an-unforked-repo.md)
|
- [Open A PR To An Unforked Repo](github/open-a-pr-to-an-unforked-repo.md)
|
||||||
- [Open File To Specific Line In Browser](github/open-file-to-specific-line-in-browser.md)
|
- [Open File To Specific Line In Browser](github/open-file-to-specific-line-in-browser.md)
|
||||||
|
- [Process JSON Output From gh With jq](github/process-json-output-from-gh-with-jq.md)
|
||||||
- [Target Another Repo When Creating A PR](github/target-another-repo-when-creating-a-pr.md)
|
- [Target Another Repo When Creating A PR](github/target-another-repo-when-creating-a-pr.md)
|
||||||
- [Tell gh What The Default Repo Is](github/tell-gh-what-the-default-repo-is.md)
|
- [Tell gh What The Default Repo Is](github/tell-gh-what-the-default-repo-is.md)
|
||||||
|
|
||||||
@@ -770,6 +772,10 @@ If you've learned something here, support my efforts writing daily TILs by
|
|||||||
- [View All Windows Of The Current App](mac/view-all-windows-of-the-current-app.md)
|
- [View All Windows Of The Current App](mac/view-all-windows-of-the-current-app.md)
|
||||||
- [Write System Clipboard To A File](mac/write-system-clipboard-to-a-file.md)
|
- [Write System Clipboard To A File](mac/write-system-clipboard-to-a-file.md)
|
||||||
|
|
||||||
|
### Math
|
||||||
|
|
||||||
|
- [Generate Permutations Of All Valid 9-ball Racks](math/generate-permutations-of-all-valid-9-ball-racks.md)
|
||||||
|
|
||||||
### Mise
|
### Mise
|
||||||
|
|
||||||
- [Create Umbrella Task For All Test Tasks](mise/create-umbrella-task-for-all-test-tasks.md)
|
- [Create Umbrella Task For All Test Tasks](mise/create-umbrella-task-for-all-test-tasks.md)
|
||||||
@@ -1068,8 +1074,10 @@ If you've learned something here, support my efforts writing daily TILs by
|
|||||||
- [Create A Range Of Descending Values](python/create-a-range-of-descending-values.md)
|
- [Create A Range Of Descending Values](python/create-a-range-of-descending-values.md)
|
||||||
- [Deduplicate A List Into A Tuple](python/deduplicate-a-list-into-a-tuple.md)
|
- [Deduplicate A List Into A Tuple](python/deduplicate-a-list-into-a-tuple.md)
|
||||||
- [Define Sequence Of Tests With Parametrize Decorator](python/define-sequence-of-tests-with-parametrize-decorator.md)
|
- [Define Sequence Of Tests With Parametrize Decorator](python/define-sequence-of-tests-with-parametrize-decorator.md)
|
||||||
|
- [Define Typed Class Interface With Protocol](python/define-typed-class-interface-with-protocol.md)
|
||||||
- [Dunder Methods](python/dunder-methods.md)
|
- [Dunder Methods](python/dunder-methods.md)
|
||||||
- [Easy Key-Value Aggregates With defaultdict](python/easy-key-value-aggregates-with-defaultdict.md)
|
- [Easy Key-Value Aggregates With defaultdict](python/easy-key-value-aggregates-with-defaultdict.md)
|
||||||
|
- [Enable Pyright Type Checking In Cursor](python/enable-pyright-type-checking-in-cursor.md)
|
||||||
- [Get Absolute Seconds From `timedelta` Object](python/get-absolute-seconds-from-timedelta-object.md)
|
- [Get Absolute Seconds From `timedelta` Object](python/get-absolute-seconds-from-timedelta-object.md)
|
||||||
- [Get Quotient And Remainder In One Operation](python/get-quotient-and-remainder-in-one-operation.md)
|
- [Get Quotient And Remainder In One Operation](python/get-quotient-and-remainder-in-one-operation.md)
|
||||||
- [Install With PIP For Specific Interpreter](python/install-with-pip-for-specific-interpreter.md)
|
- [Install With PIP For Specific Interpreter](python/install-with-pip-for-specific-interpreter.md)
|
||||||
@@ -1079,15 +1087,18 @@ If you've learned something here, support my efforts writing daily TILs by
|
|||||||
- [Load A File Into The Python REPL](python/load-a-file-into-the-python-repl.md)
|
- [Load A File Into The Python REPL](python/load-a-file-into-the-python-repl.md)
|
||||||
- [Look Inside Pytest tmp_path](python/look-inside-pytest-tmp-path.md)
|
- [Look Inside Pytest tmp_path](python/look-inside-pytest-tmp-path.md)
|
||||||
- [Make Dataclass Sortable By Specific Field](python/make-dataclass-sortable-by-specific-field.md)
|
- [Make Dataclass Sortable By Specific Field](python/make-dataclass-sortable-by-specific-field.md)
|
||||||
|
- [Make Secure Temp File For Atomic Write](python/make-secure-temp-file-for-atomic-write.md)
|
||||||
- [Override The Boolean Context Of A Class](python/override-the-boolean-context-of-a-class.md)
|
- [Override The Boolean Context Of A Class](python/override-the-boolean-context-of-a-class.md)
|
||||||
- [Parse Relative Time To datetime Object](python/parse-relative-time-to-datetime-object.md)
|
- [Parse Relative Time To datetime Object](python/parse-relative-time-to-datetime-object.md)
|
||||||
- [Reclassify Certain Packages As Dev Dependencies](python/reclassify-certain-packages-as-dev-dependencies.md)
|
- [Reclassify Certain Packages As Dev Dependencies](python/reclassify-certain-packages-as-dev-dependencies.md)
|
||||||
|
- [Set Up Pyright Type Checking In GitHub](python/set-up-pyright-type-checking-in-github.md)
|
||||||
- [Skip Specific Pytest Test Cases](python/skip-specific-pytest-test-cases.md)
|
- [Skip Specific Pytest Test Cases](python/skip-specific-pytest-test-cases.md)
|
||||||
- [Sort A List Of Dataclass Instances](python/sort-a-list-of-dataclass-instances.md)
|
- [Sort A List Of Dataclass Instances](python/sort-a-list-of-dataclass-instances.md)
|
||||||
- [Sort Normalized Version Of Data](python/sort-normalized-version-of-data.md)
|
- [Sort Normalized Version Of Data](python/sort-normalized-version-of-data.md)
|
||||||
- [Start The Debugger When A Test Errors](python/start-the-debugger-when-a-test-errors.md)
|
- [Start The Debugger When A Test Errors](python/start-the-debugger-when-a-test-errors.md)
|
||||||
- [Store And Access Immutable Data In A Tuple](python/store-and-access-immutable-data-in-a-tuple.md)
|
- [Store And Access Immutable Data In A Tuple](python/store-and-access-immutable-data-in-a-tuple.md)
|
||||||
- [Test A Function With Pytest](python/test-a-function-with-pytest.md)
|
- [Test A Function With Pytest](python/test-a-function-with-pytest.md)
|
||||||
|
- [Turn Method Into Cached Property On Class Instance](python/turn-method-into-cached-property-on-class-instance.md)
|
||||||
- [Use pipx To Install End User Apps](python/use-pipx-to-install-end-user-apps.md)
|
- [Use pipx To Install End User Apps](python/use-pipx-to-install-end-user-apps.md)
|
||||||
- [Use `__post_init__` For `dataclass` Validations](python/use-post-init-for-dataclass-validations.md)
|
- [Use `__post_init__` For `dataclass` Validations](python/use-post-init-for-dataclass-validations.md)
|
||||||
- [Use Verbose Flag To Get More Diff](python/use-verbose-flag-to-get-more-diff.md)
|
- [Use Verbose Flag To Get More Diff](python/use-verbose-flag-to-get-more-diff.md)
|
||||||
@@ -1147,6 +1158,7 @@ If you've learned something here, support my efforts writing daily TILs by
|
|||||||
- [Customize Paths And Helpers For Devise Routes](rails/customize-paths-and-helpers-for-devise-routes.md)
|
- [Customize Paths And Helpers For Devise Routes](rails/customize-paths-and-helpers-for-devise-routes.md)
|
||||||
- [Customize Template For New Schema Migration](rails/customize-template-for-new-schema-migration.md)
|
- [Customize Template For New Schema Migration](rails/customize-template-for-new-schema-migration.md)
|
||||||
- [Customize The Path Of A Resource Route](rails/customize-the-path-of-a-resource-route.md)
|
- [Customize The Path Of A Resource Route](rails/customize-the-path-of-a-resource-route.md)
|
||||||
|
- [Define Conditional Routing Logic In Routes File](rails/define-conditional-routing-logic-in-routes-file.md)
|
||||||
- [Define The Root Path For The App](rails/define-the-root-path-for-the-app.md)
|
- [Define The Root Path For The App](rails/define-the-root-path-for-the-app.md)
|
||||||
- [Delete Paranoid Records](rails/delete-paranoid-records.md)
|
- [Delete Paranoid Records](rails/delete-paranoid-records.md)
|
||||||
- [Demodulize A Class Name](rails/demodulize-a-class-name.md)
|
- [Demodulize A Class Name](rails/demodulize-a-class-name.md)
|
||||||
@@ -1563,6 +1575,7 @@ If you've learned something here, support my efforts writing daily TILs by
|
|||||||
- [Update The Gemfile Bundled With Version](ruby/update-the-gemfile-bundled-with-version.md)
|
- [Update The Gemfile Bundled With Version](ruby/update-the-gemfile-bundled-with-version.md)
|
||||||
- [Use A Case Statement As A Cond Statement](ruby/use-a-case-statement-as-a-cond-statement.md)
|
- [Use A Case Statement As A Cond Statement](ruby/use-a-case-statement-as-a-cond-statement.md)
|
||||||
- [Use dotenv In A Non-Rails Project](ruby/use-dotenv-in-a-non-rails-project.md)
|
- [Use dotenv In A Non-Rails Project](ruby/use-dotenv-in-a-non-rails-project.md)
|
||||||
|
- [Use Rescue As Part Of Inline Statement](ruby/use-rescue-as-part-of-inline-statement.md)
|
||||||
- [Use Tap For Better Test Data Setup](ruby/use-tap-for-better-test-data-setup.md)
|
- [Use Tap For Better Test Data Setup](ruby/use-tap-for-better-test-data-setup.md)
|
||||||
- [Using BCrypt To Create And Check Hashed Passwords](ruby/using-bcrypt-to-create-and-check-hashed-passwords.md)
|
- [Using BCrypt To Create And Check Hashed Passwords](ruby/using-bcrypt-to-create-and-check-hashed-passwords.md)
|
||||||
- [What To Do When You Don't Rescue](ruby/what-to-do-when-you-dont-rescue.md)
|
- [What To Do When You Don't Rescue](ruby/what-to-do-when-you-dont-rescue.md)
|
||||||
@@ -1711,6 +1724,7 @@ If you've learned something here, support my efforts writing daily TILs by
|
|||||||
- [Curl With Cookies](unix/curl-with-cookies.md)
|
- [Curl With Cookies](unix/curl-with-cookies.md)
|
||||||
- [Curling For Headers](unix/curling-for-headers.md)
|
- [Curling For Headers](unix/curling-for-headers.md)
|
||||||
- [Curling With Basic Auth Credentials](unix/curling-with-basic-auth-credentials.md)
|
- [Curling With Basic Auth Credentials](unix/curling-with-basic-auth-credentials.md)
|
||||||
|
- [Deduplicate List While Preserving Original Order](unix/deduplicate-list-while-preserving-original-order.md)
|
||||||
- [Determine ipv4 And ipv6 Public IP Addresses](unix/determine-ipv4-and-ipv6-public-ip-addresses.md)
|
- [Determine ipv4 And ipv6 Public IP Addresses](unix/determine-ipv4-and-ipv6-public-ip-addresses.md)
|
||||||
- [Diff Two Files In Unified Format](unix/diff-two-files-in-unified-format.md)
|
- [Diff Two Files In Unified Format](unix/diff-two-files-in-unified-format.md)
|
||||||
- [Different Ways To Generate A v4 UUID](unix/different-ways-to-generate-a-v4-uuid.md)
|
- [Different Ways To Generate A v4 UUID](unix/different-ways-to-generate-a-v4-uuid.md)
|
||||||
@@ -1804,6 +1818,7 @@ If you've learned something here, support my efforts writing daily TILs by
|
|||||||
- [Print A Range Of Lines For A File With Bat](unix/print-a-range-of-lines-for-a-file-with-bat.md)
|
- [Print A Range Of Lines For A File With Bat](unix/print-a-range-of-lines-for-a-file-with-bat.md)
|
||||||
- [Print DateTime Represented By Unix Timestamp](unix/print-datetime-represented-by-unix-timestamp.md)
|
- [Print DateTime Represented By Unix Timestamp](unix/print-datetime-represented-by-unix-timestamp.md)
|
||||||
- [Print Milliseconds In Human-Readable Format](unix/print-milliseconds-in-human-readable-format.md)
|
- [Print Milliseconds In Human-Readable Format](unix/print-milliseconds-in-human-readable-format.md)
|
||||||
|
- [Print Out File With Bat Without Formatting](unix/print-out-file-with-bat-without-formatting.md)
|
||||||
- [Print Out Files In Reverse](unix/print-out-files-in-reverse.md)
|
- [Print Out Files In Reverse](unix/print-out-files-in-reverse.md)
|
||||||
- [Print The Current Date In Human-Readable Format](unix/print-the-current-date-in-human-readable-format.md)
|
- [Print The Current Date In Human-Readable Format](unix/print-the-current-date-in-human-readable-format.md)
|
||||||
- [Produce A Lowercase V4 UUID](unix/produce-a-lowercase-v4-uuid.md)
|
- [Produce A Lowercase V4 UUID](unix/produce-a-lowercase-v4-uuid.md)
|
||||||
@@ -2078,6 +2093,7 @@ If you've learned something here, support my efforts writing daily TILs by
|
|||||||
- [Send A Message To A Discord Channel](workflow/send-a-message-to-a-discord-channel.md)
|
- [Send A Message To A Discord Channel](workflow/send-a-message-to-a-discord-channel.md)
|
||||||
- [Send A PDF To Your Kindle](workflow/send-a-pdf-to-your-kindle.md)
|
- [Send A PDF To Your Kindle](workflow/send-a-pdf-to-your-kindle.md)
|
||||||
- [Set Recurring Reminders In Slack](workflow/set-recurring-reminders-in-slack.md)
|
- [Set Recurring Reminders In Slack](workflow/set-recurring-reminders-in-slack.md)
|
||||||
|
- [Show All Linear Keyboard Shortcuts](workflow/show-all-linear-keyboard-shortcuts.md)
|
||||||
- [Show Linting Errors In Zed](workflow/show-linting-errors-in-zed.md)
|
- [Show Linting Errors In Zed](workflow/show-linting-errors-in-zed.md)
|
||||||
- [Temporarily Hide CleanShot X Capture Previews](workflow/temporarily-hide-cleanshot-x-capture-previews.md)
|
- [Temporarily Hide CleanShot X Capture Previews](workflow/temporarily-hide-cleanshot-x-capture-previews.md)
|
||||||
- [Toggle Between Stories In Storybook](workflow/toggle-between-stories-in-storybook.md)
|
- [Toggle Between Stories In Storybook](workflow/toggle-between-stories-in-storybook.md)
|
||||||
|
|||||||
@@ -11,6 +11,36 @@ tasks:
|
|||||||
cmds:
|
cmds:
|
||||||
- task --list
|
- task --list
|
||||||
|
|
||||||
|
browse:list:
|
||||||
|
desc: Print deduped, newest-first TIL paths
|
||||||
|
silent: true
|
||||||
|
cmds:
|
||||||
|
- |
|
||||||
|
git log --diff-filter=A --name-only --pretty=format: -- '*/*.md' \
|
||||||
|
| grep -v '^$' \
|
||||||
|
| awk '!seen[$0]++'
|
||||||
|
|
||||||
|
browse:
|
||||||
|
desc: Pick from 5 most recent TILs (fzf) and open in browser
|
||||||
|
interactive: true
|
||||||
|
silent: true
|
||||||
|
cmds:
|
||||||
|
- |
|
||||||
|
FILE=$(task browse:list | head -5 | fzf --prompt="Open TIL: " --height=40% --reverse) || true
|
||||||
|
if [ -n "$FILE" ]; then
|
||||||
|
gh browse "$FILE"
|
||||||
|
fi
|
||||||
|
|
||||||
|
browse:latest:
|
||||||
|
desc: Open the single most recent TIL in the browser
|
||||||
|
silent: true
|
||||||
|
cmds:
|
||||||
|
- |
|
||||||
|
FILE=$(task browse:list | head -1)
|
||||||
|
if [ -n "$FILE" ]; then
|
||||||
|
gh browse "$FILE"
|
||||||
|
fi
|
||||||
|
|
||||||
notes:
|
notes:
|
||||||
desc: Interactive picker for notes tasks
|
desc: Interactive picker for notes tasks
|
||||||
cmds:
|
cmds:
|
||||||
|
|||||||
@@ -0,0 +1,58 @@
|
|||||||
|
# Process JSON Output From gh With jq
|
||||||
|
|
||||||
|
The `gh` (GitHub) CLI is useful for accessing data about your profile and
|
||||||
|
projects from the terminal. With the `--json` flag, we can access the data in a
|
||||||
|
structured way which is useful for scripting.
|
||||||
|
|
||||||
|
Here is an example of pulling a list of all my repositories, limiting each
|
||||||
|
entity to just the `nameWithOwner` and `description`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
❯ gh repo list --limit 1000 --json nameWithOwner,description
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"description": "My personal site -- joshbranchaud.com",
|
||||||
|
"nameWithOwner": "jbranchaud/personal-site"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Private repo for the NOTES.md of my TIL repo",
|
||||||
|
"nameWithOwner": "jbranchaud/til-notes-private"
|
||||||
|
},
|
||||||
|
...
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
If I'm using the `--json` flag, then I can add in the `--jq` flag to apply a
|
||||||
|
`jq` query for additional processing of the output.
|
||||||
|
|
||||||
|
Here I convert it to a series of tuples:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
❯ gh repo list --limit 1000 --json nameWithOwner,description \
|
||||||
|
--jq '.[] | [.nameWithOwner, .description]'
|
||||||
|
[
|
||||||
|
"jbranchaud/personal-site",
|
||||||
|
"My personal site -- joshbranchaud.com"
|
||||||
|
]
|
||||||
|
[
|
||||||
|
"jbranchaud/til-notes-private",
|
||||||
|
"Private repo for the NOTES.md of my TIL repo"
|
||||||
|
]
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
Then I can add one more pipe to that `jq` query to turn it into _tab-separated
|
||||||
|
values_ using
|
||||||
|
[`@tsv`](https://jqlang.org/manual/v1.5/#format-strings-and-escaping):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
❯ gh repo list --limit 1000 --json nameWithOwner,description \
|
||||||
|
--jq '.[] | [.nameWithOwner, .description] | @tsv'
|
||||||
|
jbranchaud/personal-site My personal site -- joshbranchaud.com
|
||||||
|
jbranchaud/til-notes-private Private repo for the NOTES.md of my TIL repo
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
This is useful because I can then pipe it to another program, such as an `fzf`
|
||||||
|
command like [this repo selector that opens the selected one in the
|
||||||
|
browser](https://github.com/jbranchaud/dotfiles/commit/f964ca10c6c4db3475411c2991dc2f1dfd18c818).
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
# Generate Permutations Of All Valid 9-ball Racks
|
||||||
|
|
||||||
|
I wanted to produce a full listing of all valid rack arrangements for the game
|
||||||
|
of [9-ball](https://en.wikipedia.org/wiki/Nine-ball). The constraints on how a
|
||||||
|
9-ball rack can be arranged are, first, that the 1 ball must be placed at the
|
||||||
|
head of the diamond and, second, that the 9 ball must be placed at the center of
|
||||||
|
the diamond. After that, all other balls (2 through 8) can be placed in any
|
||||||
|
arrangement.
|
||||||
|
|
||||||
|
Because each of those seven remaining balls can be arranged in distinct
|
||||||
|
orderings where each ball is placed once, this is a
|
||||||
|
[_permutation_](https://en.wikipedia.org/wiki/Permutation) problem.
|
||||||
|
|
||||||
|
> In elementary combinatorics, the k-permutations, or partial permutations, are
|
||||||
|
> the ordered arrangements of k distinct elements selected from a set. When k is
|
||||||
|
> equal to the size of the set, these are the permutations in the previous
|
||||||
|
> sense.
|
||||||
|
|
||||||
|
For this problem, the seven distinct elements can be arranged into `7!` (seven
|
||||||
|
factorial) unique permutations. That is, 5040 permutations.
|
||||||
|
|
||||||
|
I can use [Ruby's `Array#permutations`
|
||||||
|
method](https://docs.ruby-lang.org/en/4.0/Array.html#method-i-permutation) to
|
||||||
|
enumerate these 5040 permutations like so:
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
[2,3,4,5,6,7,8].permutation.map do |perm|
|
||||||
|
[1, *perm[0..2], 9, *perm[3..7]]
|
||||||
|
end.to_a
|
||||||
|
=> [[1, 2, 3, 4, 9, 5, 6, 7, 8],
|
||||||
|
[1, 2, 3, 4, 9, 5, 6, 8, 7],
|
||||||
|
[1, 2, 3, 4, 9, 5, 7, 6, 8],
|
||||||
|
[1, 2, 3, 4, 9, 5, 7, 8, 6],
|
||||||
|
[1, 2, 3, 4, 9, 5, 8, 6, 7],
|
||||||
|
[1, 2, 3, 4, 9, 5, 8, 7, 6],
|
||||||
|
[1, 2, 3, 4, 9, 6, 5, 7, 8],
|
||||||
|
...
|
||||||
|
[1, 8, 7, 6, 9, 5, 3, 2, 4],
|
||||||
|
[1, 8, 7, 6, 9, 5, 3, 4, 2],
|
||||||
|
[1, 8, 7, 6, 9, 5, 4, 2, 3],
|
||||||
|
[1, 8, 7, 6, 9, 5, 4, 3, 2]]
|
||||||
|
```
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
# Define Typed Class Interface With Protocol
|
||||||
|
|
||||||
|
In [`py-vmt`](https://github.com/jbranchaud/py-vmt) I am defining different
|
||||||
|
storage access layers for the CLI to use. I want a consistent interface that the
|
||||||
|
core CLI logic can depend on regardless of whether it is a JSON file or a SQLite
|
||||||
|
database. To achieve that I can define a class of unimplemented functions that
|
||||||
|
inherits from
|
||||||
|
[`typing.Protocol`](https://typing.python.org/en/latest/spec/protocol.html).
|
||||||
|
|
||||||
|
```python
|
||||||
|
from typing import Protocol
|
||||||
|
|
||||||
|
class SessionRepository(Protocol):
|
||||||
|
def active_session(self) -> Session | None: ...
|
||||||
|
def write_active_session(self, session) -> None: ...
|
||||||
|
def append_session(self, session) -> None: ...
|
||||||
|
def all_sessions(self) -> list[Session]: ...
|
||||||
|
def clear_active_session(self) -> None: ...
|
||||||
|
```
|
||||||
|
|
||||||
|
Notice that none of these have default implementations. The `...` indicates that
|
||||||
|
class implementing this protocol will define the implementation of those
|
||||||
|
functions.
|
||||||
|
|
||||||
|
Now, my `CliContext` class, which needs some kind of `SessionRepository` to
|
||||||
|
function can indicate as much in `__init__`.
|
||||||
|
|
||||||
|
```python
|
||||||
|
class CliContext:
|
||||||
|
def __init__(self, verbose: bool, repo: SessionRepository | None = None) -> None:
|
||||||
|
self.verbose: bool = verbose
|
||||||
|
self.active_session: Session | None = None
|
||||||
|
self.repo: SessionRepository = repo or JsonRepository()
|
||||||
|
self.active_session = self.repo.active_session()
|
||||||
|
```
|
||||||
|
|
||||||
|
If `JsonRepository` doesn't define all of the methods specified in the protocol,
|
||||||
|
then a type error will occur wherever it clashes with `SessionRepository`. Now
|
||||||
|
as I implement `SqliteRepository` I have a standard interface to build against
|
||||||
|
that I know I can seamlessly swap in.
|
||||||
|
|
||||||
|
[source](https://typing.python.org/en/latest/reference/protocols.html#simple-user-defined-protocols)
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
# Enable Pyright Type Checking In Cursor
|
||||||
|
|
||||||
|
In most ways [Cursor](https://cursor.com/), a clone of VS Code, behaves like VS
|
||||||
|
Code and uses the same extensions as VS Code. It even offers to clone all your
|
||||||
|
existing extensions and setup from VS Code when you first install it.
|
||||||
|
|
||||||
|
However, the Pyright type checking setup that I had in VS Code stopped working
|
||||||
|
when I opened up the same Python project in Cursor. It seems that to get Pyright
|
||||||
|
to reliably work with forks of VS Code, you need to use a compatible fork like
|
||||||
|
[Based Pyright](https://docs.basedpyright.com/latest/).
|
||||||
|
|
||||||
|
Once I installed _Based Pyright_ from the extension marketplace, I was able to
|
||||||
|
enable it in `.vscode/settings.json`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
...,
|
||||||
|
"basedpyright.analysis.typeCheckingMode": "basic"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
I may have needed to restart Cursor at this point, I cannot remember exactly.
|
||||||
|
However, once this setup was in place the helpful type checking errors started
|
||||||
|
appearing as red squiggles.
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
# Make Secure Temp File For Atomic Write
|
||||||
|
|
||||||
|
Two types of failure modes that can occur while writing to a shared file on the
|
||||||
|
file system are 1) a corrupted file due to a crash mid-write and 2) another
|
||||||
|
process reading a partial file mid-write.
|
||||||
|
|
||||||
|
One way I've handled this in [`py-vmt`](https://github.com/jbranchaud/py-vmt) is
|
||||||
|
to perform the write operations on a secure temp file and then use the OS-level
|
||||||
|
atomic `rename` operation. I do this by [creating a
|
||||||
|
`contextmanager`](https://docs.python.org/3/library/contextlib.html#contextlib.contextmanager)
|
||||||
|
that uses
|
||||||
|
[`tempfile.mkstemp`](https://docs.python.org/3/library/tempfile.html#tempfile.mkstemp)
|
||||||
|
and [`os.replace`](https://docs.python.org/3/library/os.html#os.replace).
|
||||||
|
|
||||||
|
Here is what the `contextmanager` looks like:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from contextlib import contextmanager
|
||||||
|
from pathlib import Path
|
||||||
|
import os, tempfile
|
||||||
|
|
||||||
|
@contextmanager
|
||||||
|
def atomic_write(path: Path):
|
||||||
|
# write to a tmp file in the same directory, then atomically swap it
|
||||||
|
fd, temp_file_path = tempfile.mkstemp(dir=path.parent, suffix=".tmp")
|
||||||
|
try:
|
||||||
|
with os.fdopen(fd, "w") as file:
|
||||||
|
yield file
|
||||||
|
os.replace(temp_file_path, path)
|
||||||
|
except BaseException:
|
||||||
|
os.unlink(temp_file_path)
|
||||||
|
raise
|
||||||
|
```
|
||||||
|
|
||||||
|
This explicitly creates a secure temp file in the same directory as the given
|
||||||
|
path with `.tmp` as the suffix. I then open the file descriptor using the
|
||||||
|
`os.fdopen` context manager (which will manage closing the file descriptor for
|
||||||
|
me). The `@contextmanager` decorator plus the `yield file` are what allow this
|
||||||
|
to be used as a `with` block. Once any file operations are done, then I use
|
||||||
|
`os.replace` to atomically swap out the original file with the temp file.
|
||||||
|
|
||||||
|
Here is how I use it to write updates to JSON data files:
|
||||||
|
|
||||||
|
```python
|
||||||
|
def write_active_session(self, session: Session) -> None:
|
||||||
|
with atomic_write(self.active_session_file) as file:
|
||||||
|
json.dump(session.marshal(), file)
|
||||||
|
```
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
# Set Up Pyright Type Checking In GitHub
|
||||||
|
|
||||||
|
As I get into more of a PR workflow with my development of
|
||||||
|
[`py-vmt`](https://github.com/jbranchaud/py-vmt), I need to set up some basic CI
|
||||||
|
checks in GitHub. For starters I want the same `pyright` type checking that I
|
||||||
|
have locally to be run in CI for consistency.
|
||||||
|
|
||||||
|
Though my editor is set up to do Pyright type checking as I work locally, I can
|
||||||
|
also manually run it with:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ uv run pyright
|
||||||
|
```
|
||||||
|
|
||||||
|
Pyright will look for the `tool.pyright` section in my `pyproject.toml` file
|
||||||
|
which currently looks like the following:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[tool.pyright]
|
||||||
|
include = ["src", "tests"]
|
||||||
|
```
|
||||||
|
|
||||||
|
I can get this same type checking in CI for PRs by adding the following
|
||||||
|
`.github/workflows/typecheck.yml` file:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
name: pyright
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
typecheck:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install uv
|
||||||
|
uses: astral-sh/setup-uv@v3
|
||||||
|
with:
|
||||||
|
enable-cache: true
|
||||||
|
|
||||||
|
- name: Set up Python
|
||||||
|
run: uv python install
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: uv sync --all-extras --dev
|
||||||
|
|
||||||
|
- name: Run pyright
|
||||||
|
run: uv run pyright
|
||||||
|
```
|
||||||
|
|
||||||
|
This adds a single `typecheck` job that installs `uv`, `python`, and my project
|
||||||
|
dependencies, and then runs `uv run pyright` (just like I do locally) to perform
|
||||||
|
type checking. If `pyright` discovers any type errors, the job will fail and I
|
||||||
|
can view the output of the job to see what needs fixing. Once I have dealt with
|
||||||
|
everything, the job will quietly pass with a green check mark.
|
||||||
|
|
||||||
|
Here is [the PR](https://github.com/jbranchaud/py-vmt/pull/2) where I added this
|
||||||
|
CI job.
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
# Turn Method Into Cached Property On Class Instance
|
||||||
|
|
||||||
|
I have a class that encapsulates a few things including a somewhat expensive
|
||||||
|
data lookup from a file on disk. When this class is instantiated, it is
|
||||||
|
short-lived and the data that gets pulled from the file on disk is considered
|
||||||
|
fresh for the life of the instance.
|
||||||
|
|
||||||
|
```python
|
||||||
|
class CliContext:
|
||||||
|
def __init__(self, verbose: bool) -> None:
|
||||||
|
# ...
|
||||||
|
self.repo = JsonRepository()
|
||||||
|
# ...
|
||||||
|
|
||||||
|
def session_log(self) -> list[Session]:
|
||||||
|
return self.repo.load_session_log()
|
||||||
|
```
|
||||||
|
|
||||||
|
Because this method gets called from a couple places during a single lifecycle,
|
||||||
|
this class would benefit from caching it via the [`@cached_property`
|
||||||
|
decorator](https://docs.python.org/3/library/functools.html#functools.cached_property).
|
||||||
|
|
||||||
|
```python
|
||||||
|
from functools import cached_property
|
||||||
|
|
||||||
|
class CliContext:
|
||||||
|
def __init__(self, verbose: bool) -> None:
|
||||||
|
# ...
|
||||||
|
self.repo = JsonRepository()
|
||||||
|
# ...
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def session_log(self) -> list[Session]:
|
||||||
|
return self.repo.load_session_log()
|
||||||
|
```
|
||||||
|
|
||||||
|
Now `session_log` can be treated like a property instead of a method. That means
|
||||||
|
when I want to load and access the session log, I can do `self.session_log` (no
|
||||||
|
parentheses) like I would any other property. The first time I reference it, the
|
||||||
|
method will run. Then that value will be cached and all subsequent references
|
||||||
|
will use that cache.
|
||||||
|
|
||||||
|
> Transform a method of a class into a property whose value is computed once and
|
||||||
|
> then cached as a normal attribute for the life of the instance.
|
||||||
|
|
||||||
|
Of course, anytime we use caching, we can create a footgun for ourselves. We
|
||||||
|
have to be careful that our program doesn't evolve in such a way where the
|
||||||
|
caching will create a subtle bug due to stale data.
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
# Define Conditional Routing Logic In Routes File
|
||||||
|
|
||||||
|
I ran into a situation recently where I needed to intercept the behavior a
|
||||||
|
common public-facing route in an app. Broadly, the route is for company specific
|
||||||
|
rental pages with query parameters that correspond to their available inventory.
|
||||||
|
|
||||||
|
What I needed was a way to display a demo version of that rental page ignoring
|
||||||
|
everything else about how the request would otherwise be processed, validated,
|
||||||
|
and rendered.
|
||||||
|
|
||||||
|
Instead of introducing a bunch of weird conditional logic into this already
|
||||||
|
complex rental controller, I was able to intercept the request at the routing
|
||||||
|
layer when `demo=true` is set and send it to a different controller.
|
||||||
|
|
||||||
|
Here is what that section of `config/routes.rb` looks like:
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
get "rentals/new", to: "rental_demos#show",
|
||||||
|
as: :rental_demo,
|
||||||
|
constraints: ->(request) { request.params[:demo] == "true" }
|
||||||
|
|
||||||
|
resources :rentals, only: %i[new create] do
|
||||||
|
# ...
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
This specifies a `constraint` on the `get` handler matching for a given request.
|
||||||
|
If the constraint isn't met, then the route handling logic proceeds where it
|
||||||
|
will instead find a match with the original new rentals resource routing.
|
||||||
|
|
||||||
|
Now I can reference a version of this URL that includes `demo=true` as a way of
|
||||||
|
having an always-available realistic-looking version of the rental page even if
|
||||||
|
one of these companies doesn't actively have available inventory.
|
||||||
|
|
||||||
|
Those requests will get intercepted by the first matching route handler which
|
||||||
|
will send them to the `RentalDemosController` instead of the
|
||||||
|
`RentalsController`.
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
# Use Rescue As Part Of Inline Statement
|
||||||
|
|
||||||
|
In Ruby I typically think of `rescue` as block syntax that I can use to handle
|
||||||
|
exceptions.
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
begin
|
||||||
|
User.update!(password:)
|
||||||
|
rescue
|
||||||
|
puts "There was an issue updating the password"
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
The `rescue` keyword can also be used as part of an inline statement as a way of
|
||||||
|
providing a _fallback_ value when the first part of the statement raises.
|
||||||
|
|
||||||
|
For instance, if I'm trying to access some value on an array that happens to be
|
||||||
|
`nil`, it is going to raise:
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
> scores.first
|
||||||
|
(irb):7:in '<main>': undefined method 'first' for nil (NoMethodError)
|
||||||
|
```
|
||||||
|
|
||||||
|
I can instead tack on a `rescue 0` which will give it `0` as a fallback value:
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
> scores.first rescue 0
|
||||||
|
=> 0
|
||||||
|
```
|
||||||
|
|
||||||
|
Of course, there are more idiomatic ways to handle this kind of situation in
|
||||||
|
Ruby. Maybe something like this:
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
> Array(scores).first || 0
|
||||||
|
=> 0
|
||||||
|
```
|
||||||
|
|
||||||
|
Another way I've seen this inline rescue used is to print out the exception
|
||||||
|
caused by that line of code, using `$!` (the global variable for the most
|
||||||
|
recently raised exception).
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
> scores.first rescue puts $!
|
||||||
|
undefined method 'first' for nil
|
||||||
|
=> nil
|
||||||
|
```
|
||||||
|
|
||||||
|
That is a one-liner for the following:
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
begin
|
||||||
|
scores.first
|
||||||
|
rescue => e
|
||||||
|
puts e
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
The big caveat that goes with this is the same one that goes with any other
|
||||||
|
blanket `rescue` block. If you are indiscriminately rescuing exceptions without
|
||||||
|
being intentional about what you are rescuing and why, you could be potentially
|
||||||
|
burying exceptions that you need to know about.
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
# Deduplicate List While Preserving Original Order
|
||||||
|
|
||||||
|
Usually when I want to deduplicate a list coming out of some command, I'll reach
|
||||||
|
for `sort | uniq`. This is a nice Unix trick where `uniq` removes consecutive
|
||||||
|
duplicate lines which relies on `sort` first reorganizing all lines in
|
||||||
|
alphabetically sorted order, bringing all duplicate lines together.
|
||||||
|
|
||||||
|
The caveat to using `sort | uniq` (or even `sort -u`) is that it will reorder
|
||||||
|
entries alphabetically. That means you'll lose the original order, which may
|
||||||
|
have been important.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
❯ echo "red green blue red yellow green blue red green" | tr ' ' '\n' | sort -u
|
||||||
|
blue
|
||||||
|
green
|
||||||
|
red
|
||||||
|
yellow
|
||||||
|
```
|
||||||
|
|
||||||
|
Another approach is to use `awk` which can deduplicate while preserving the
|
||||||
|
order of entries as they first appear. This can be done with a pattern that
|
||||||
|
records the count of each line in an associative array.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
❯ echo "red green blue red yellow green blue red green" | tr ' ' '\n' | awk '!seen[$0]++'
|
||||||
|
red
|
||||||
|
green
|
||||||
|
blue
|
||||||
|
yellow
|
||||||
|
```
|
||||||
|
|
||||||
|
The above pattern accepts on the first occurrence of each line and rejects on
|
||||||
|
any subsequent occurrences. That is done by adding `$0` (the current line) to
|
||||||
|
`seen` (associative array that auto-initializes inline). If it doesn't exist in
|
||||||
|
`seen` yet, then `0` is returned which is negated to a truthy value with `!`.
|
||||||
|
That entry is then incremented from `0` to `1` via the `++`. As `awk` continues
|
||||||
|
to process each line, `seen` is continually added to and incremented. The
|
||||||
|
default _action_ for `awk` is to print the line. Those truthy lines are the ones
|
||||||
|
that are printed.
|
||||||
|
|
||||||
|
An example of where this might be useful is when creating a unique listing of
|
||||||
|
all authors of a git repository while maintaining the order that they become
|
||||||
|
committers. I wanted to show this with a high-contribution public repo that I
|
||||||
|
worked on, so I referenced the [`egghead-next`
|
||||||
|
repo](https://github.com/skillrecordings/egghead-next).
|
||||||
|
|
||||||
|
```bash
|
||||||
|
❯ git log --reverse --format='%an <%ae>' | awk '!seen[$0]++'
|
||||||
|
Joel Hooks <joelhooks@gmail.com>
|
||||||
|
johnlindquist <johnlindquist@gmail.com>
|
||||||
|
John Lindquist <johnlindquist@gmail.com>
|
||||||
|
William Johnson <w.alexander.johnson@gmail.com>
|
||||||
|
depfu[bot] <23717796+depfu[bot]@users.noreply.github.com>
|
||||||
|
Evgeniy Nagalskiy <evgeniy.nagalskiy@gmail.com>
|
||||||
|
Taylor Bell <taylorbell@gmail.com>
|
||||||
|
Maggie Appleton <maggie.fm.appleton@gmail.com>
|
||||||
|
John Lindquist <johnlindquist@work.local>
|
||||||
|
Vojta Holik <vojta@egghead.io>
|
||||||
|
Daniel Miller <dealingwith@gmail.com>
|
||||||
|
jh3y <jh3y@users.noreply.github.com>
|
||||||
|
Jhey Tompkins <jh3y@users.noreply.github.com>
|
||||||
|
Josh Branchaud <jbranchaud@gmail.com>
|
||||||
|
Lauro Silva <57044804+laurosilvacom@users.noreply.github.com>
|
||||||
|
LB <barth.laurie@gmail.com>
|
||||||
|
kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
|
||||||
|
dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
|
||||||
|
samuelhulick <samuel@samuelhulick.com>
|
||||||
|
Ian Jones <jones58ian@gmail.com>
|
||||||
|
Zac Jones <zacjones93@gmail.com>
|
||||||
|
...
|
||||||
|
```
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
# Print Out File With Bat Without Formatting
|
||||||
|
|
||||||
|
The [`bat`](https://github.com/sharkdp/bat) utility is my daily driver and
|
||||||
|
replacement for anything used `cat` for before. I even have `bat` aliased to
|
||||||
|
`cat` so that I never had to rewire my muscle memory for typing `cat`.
|
||||||
|
|
||||||
|
Whether or not the creator of `cat` intended it, I'd guess that most terminal
|
||||||
|
users' main use case is printing the contents of a file. `bat` does that way
|
||||||
|
better with syntax highlighting, line numbers, and some layout formatting that
|
||||||
|
puts lines around the output and a heading with the filename.
|
||||||
|
|
||||||
|
All this formatting is great when I'm taking a quick look at a file. One way it
|
||||||
|
gets in the way is when I'm trying to highlight and copy a few lines to my
|
||||||
|
clipboard. Because the terminal is rendering lines, line numbers, and other
|
||||||
|
formatting, all that fluff gets included on the clipboard.
|
||||||
|
|
||||||
|
For this scenario, I can use the `-p` flag (or `--style=plain`) to print just
|
||||||
|
the (syntax-highlighted) file contents without all the extra formatting.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bat -p app/models/users.rb
|
||||||
|
|
||||||
|
# or
|
||||||
|
bat --style=plain app/models/users.rb
|
||||||
|
```
|
||||||
|
|
||||||
|
Another way I could have approached this was to [ignore the alias of `cat` to
|
||||||
|
`bat`](ignore-the-alias-when-running-a-command.md).
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
# Show All Linear Keyboard Shortcuts
|
||||||
|
|
||||||
|
Linear, the project management software, puts an incredible amount of attention
|
||||||
|
to detail into the UX and UI of their app. This includes making the app a power
|
||||||
|
tool for power users with tons of keyboard shortcuts.
|
||||||
|
|
||||||
|
I'm aware of some of Linear's keyboard shortcuts, but the discoverability of
|
||||||
|
many of them is tough.
|
||||||
|
|
||||||
|
A great way to list and browse through all of them right in the app is with
|
||||||
|
`Cmd+/`.
|
||||||
|
|
||||||
|
They are organized into sections that I can scroll through. There is also a
|
||||||
|
search box at the top of this _Keyboard Shortcuts_ panel where I can narrow down
|
||||||
|
the results to those that match a term.
|
||||||
|
|
||||||
|
A few that I'm finding immediately useful are:
|
||||||
|
|
||||||
|
- `gi` to go to my _Inbox_ in the current workspace
|
||||||
|
- `gm` to go to _My Issues_ in the current workspace
|
||||||
|
- `ow` to open a picker to switch between workspaces
|
||||||
|
|
||||||
|
Note: the _Keyboard Shortcuts_ panel lists many of the letter-based shortcuts as
|
||||||
|
being capitalized. I've found that these don't work when I hold shift. For that
|
||||||
|
reason, I've listed the above shortcuts with lowercase letters.
|
||||||
Reference in New Issue
Block a user