11 |
--------------------------------------------------------------------------------
/expansions/edit_javascript.py:
--------------------------------------------------------------------------------
1 | from javascript_data_files import read_js, write_js
2 |
3 | value = read_js("filename.js", varname="varname")
4 |
5 | write_js("filename.js", varname="varname", value=value)
6 |
--------------------------------------------------------------------------------
/expansions/datetime_encoder.py:
--------------------------------------------------------------------------------
1 | import datetime
2 |
3 |
4 | class DatetimeEncoder(json.JSONEncoder):
5 | def default(self, obj):
6 | if isinstance(obj, datetime.datetime):
7 | return obj.isoformat()
8 |
--------------------------------------------------------------------------------
/expansions/list_s3_objects.py:
--------------------------------------------------------------------------------
1 | def list_s3_objects(sess: boto3.Session, **kwargs):
2 | s3 = sess.client("s3")
3 |
4 | for page in s3.get_paginator("list_objects_v2").paginate(**kwargs):
5 | yield from page.get("Contents", [])
6 |
--------------------------------------------------------------------------------
/expansions/flapi.py:
--------------------------------------------------------------------------------
1 | from flickr_api import FlickrApi
2 | import keyring
3 |
4 | api = FlickrApi.with_api_key(
5 | api_key=keyring.get_password("flickr_api", "key"),
6 | user_agent="Alex Chan's personal scripts ",
7 | )
8 |
--------------------------------------------------------------------------------
/info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | snippetkeywordprefix
6 |
7 | snippetkeywordsuffix
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/expansions/get_secrets_manager_secret.py:
--------------------------------------------------------------------------------
1 | def get_secret_string(sess: boto3.Session, **kwargs) -> str:
2 | """
3 | Look up a SecretString from Secrets Manager, and return the string.
4 | """
5 | secrets = sess.client("secretsmanager")
6 |
7 | resp = secrets.get_secret_value(**kwargs)
8 |
9 | return resp["SecretString"]
10 |
--------------------------------------------------------------------------------
/expansions/template.svg:
--------------------------------------------------------------------------------
1 |
14 |
--------------------------------------------------------------------------------
/expansions/create_hash.py:
--------------------------------------------------------------------------------
1 | import hashlib
2 | from pathlib import Path
3 |
4 |
5 | def create_hash(path: Path) -> "hashlib._Hash":
6 | """
7 | Returns the checksum of the given path.
8 | """
9 | h = hashlib.sha256()
10 |
11 | with open(path, "rb") as infile:
12 | while chunk := infile.read(8192):
13 | h.update(chunk)
14 |
15 | return h
16 |
--------------------------------------------------------------------------------
/dev_requirements.txt:
--------------------------------------------------------------------------------
1 | # This file was autogenerated by uv via the following command:
2 | # uv pip compile dev_requirements.in --output-file dev_requirements.txt
3 | mypy==1.18.2
4 | # via -r dev_requirements.in
5 | mypy-extensions==1.0.0
6 | # via mypy
7 | pathspec==0.12.1
8 | # via mypy
9 | ruff==0.13.1
10 | # via -r dev_requirements.in
11 | tomli==2.2.1
12 | # via mypy
13 | typing-extensions==4.13.1
14 | # via mypy
15 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: "github-actions"
4 | directory: "/"
5 | schedule:
6 | interval: "weekly"
7 | day: "monday"
8 | time: "09:00"
9 | - package-ecosystem: "pip"
10 | directory: "/"
11 | schedule:
12 | interval: "weekly"
13 | day: "monday"
14 | time: "09:00"
15 | ignore:
16 | - dependency-name: "mypy"
17 | - dependency-name: "ruff"
18 |
--------------------------------------------------------------------------------
/expansions/get_boto3_session.py:
--------------------------------------------------------------------------------
1 | import boto3
2 |
3 |
4 | def get_aws_session(*, role_arn: str) -> boto3.Session:
5 | sts_client = boto3.client("sts")
6 | assumed_role_object = sts_client.assume_role(
7 | RoleArn=role_arn, RoleSessionName="AssumeRoleSession1"
8 | )
9 | credentials = assumed_role_object["Credentials"]
10 |
11 | return boto3.Session(
12 | aws_access_key_id=credentials["AccessKeyId"],
13 | aws_secret_access_key=credentials["SecretAccessKey"],
14 | aws_session_token=credentials["SessionToken"],
15 | )
16 |
--------------------------------------------------------------------------------
/expansions/get_directories.py:
--------------------------------------------------------------------------------
1 | import pathlib
2 |
3 |
4 | def get_directories_under(root="."):
5 | """
6 | Generates the absolute paths to every directory under ``root``.
7 | """
8 | root = pathlib.Path(root)
9 |
10 | if root.exists() and not root.is_dir():
11 | raise ValueError(f"Cannot find files under file: {root!r}")
12 |
13 | if not root.is_dir():
14 | raise FileNotFoundError(root)
15 |
16 | for dirpath, _, _ in root.walk():
17 | yield dirpath
18 |
19 |
20 | for p in get_directories_under():
21 | {cursor}
22 |
--------------------------------------------------------------------------------
/expansions/list_dynamodb_rows.py:
--------------------------------------------------------------------------------
1 | import boto3
2 |
3 |
4 | def scan_table(sess: boto3.Session, *, TableName: str, **kwargs):
5 | """
6 | Generates all the items in a DynamoDB table.
7 |
8 | :param dynamo_client: A boto3 client for DynamoDB.
9 | :param TableName: The name of the table to scan.
10 |
11 | Other keyword arguments will be passed directly to the Scan operation.
12 | See https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/dynamodb.html#DynamoDB.Client.scan
13 |
14 | """
15 | dynamo_client = sess.resource("dynamodb").meta.client
16 | paginator = dynamo_client.get_paginator("scan")
17 |
18 | for page in paginator.paginate(TableName=TableName, **kwargs):
19 | yield from page["Items"]
20 |
--------------------------------------------------------------------------------
/.github/workflows/test.yml:
--------------------------------------------------------------------------------
1 | name: Test
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 |
8 | pull_request:
9 | branches:
10 | - main
11 |
12 | jobs:
13 | test:
14 | runs-on: ubuntu-latest
15 |
16 | steps:
17 | - uses: actions/checkout@v6
18 |
19 | - name: Set up Python
20 | uses: actions/setup-python@v6
21 | with:
22 | python-version: 3.12
23 | cache: pip
24 | cache-dependency-path: dev_requirements.txt
25 |
26 | - name: Install dependencies
27 | run: pip install -r dev_requirements.txt
28 |
29 | - name: Run linting
30 | run: |
31 | ruff check *.py
32 | ruff format --check .
33 |
34 | - name: Check types
35 | run: mypy *.py
36 |
37 | - name: Build the workflow
38 | run: python3 create_snippets.py
39 |
--------------------------------------------------------------------------------
/expansions/get_file_paths.py:
--------------------------------------------------------------------------------
1 | from collections.abc import Iterator
2 | from pathlib import Path
3 |
4 |
5 | def get_file_paths_under(
6 | root: Path | str = Path("."), *, suffix: str = ""
7 | ) -> Iterator[Path]:
8 | """
9 | Generates the absolute paths to every matching file under ``root``.
10 | """
11 | root = Path(root)
12 |
13 | if root.exists() and not root.is_dir():
14 | raise ValueError(f"Cannot find files under non-directory: {root!r}")
15 |
16 | if not root.is_dir():
17 | raise FileNotFoundError(root)
18 |
19 | for dirpath, _, filenames in root.walk():
20 | for f in filenames:
21 | p = dirpath / f
22 |
23 | if p.is_file() and f.lower().endswith(suffix):
24 | yield p
25 |
26 |
27 | for p in get_file_paths_under():
28 | {cursor}
29 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2024 Alex Chan
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a
4 | copy of this software and associated documentation files (the "Software"),
5 | to deal in the Software without restriction, including without limitation
6 | the rights to use, copy, modify, merge, publish, distribute, sublicense,
7 | and/or sell copies of the Software, and to permit persons to whom the Software
8 | is furnished to do so, subject to the following conditions:
9 |
10 | The above copyright notice and this permission notice shall be included in
11 | all copies or substantial portions of the Software.
12 |
13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
16 | THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
17 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
18 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
19 | OTHER DEALINGS IN THE SOFTWARE.
20 |
--------------------------------------------------------------------------------
/expansions/mit_license.txt:
--------------------------------------------------------------------------------
1 | Copyright (c) {isodate:yyyy} Alex Chan
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a
4 | copy of this software and associated documentation files (the "Software"),
5 | to deal in the Software without restriction, including without limitation
6 | the rights to use, copy, modify, merge, publish, distribute, sublicense,
7 | and/or sell copies of the Software, and to permit persons to whom the Software
8 | is furnished to do so, subject to the following conditions:
9 |
10 | The above copyright notice and this permission notice shall be included in
11 | all copies or substantial portions of the Software.
12 |
13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
16 | THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
17 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
18 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
19 | OTHER DEALINGS IN THE SOFTWARE.
20 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # snippets
2 |
3 | This repo has my (public) text expansion snippets.
4 |
5 | Text expansion apps allow me to save keystrokes when I'm typing common words – for example, if I type `intl`, my computer expands it to `international`.
6 | That saves me 9 keystrokes.
7 | It may not seem like much, but I use these snippets dozens of times a day and it quickly adds up!
8 |
9 | This repo has a script that creates a collection of snippets to use [in Alfred](https://www.alfredapp.com/help/features/snippets/).
10 |
11 | ## Why not use Alfred's in-app snippet editor?
12 |
13 | Because I want to share snippets between my home and work computers.
14 |
15 | Alfred can sync its preferences through some sort of cloud storage, e.g. Dropbox or iCloud.
16 | But I don't want to connect my home and work computers that way – I try to keep them separate.
17 | e.g. I don't log into my work email on my home computer, and I don't log in to my personal iCloud on my work laptop.
18 |
19 | By putting my snippets in a GitHub repo, I can check out the repo on both computers and get the same set of snippets, but in a way that maintains the gap between the machines.
20 | Neither machine can directly affect the other.
21 |
22 | I don't put all my snippets in this repo; just the ones I can make public.
23 | For example, my home computer has extra snippets for personal info like my phone number and my address.
24 | I configure those in the in-app editor, because I don't want to put them in a public Git repo and I don't need them on my work computer.
25 |
26 | ## Usage
27 |
28 | ```console
29 | $ python3 create_snippets.py
30 | ```
31 |
32 | This will create a new file `Alex’s snippets.alfredsnippets`, which you can open to add these snippets to Alfred Preferences.
33 |
--------------------------------------------------------------------------------
/create_snippets.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 |
3 | import hashlib
4 | import json
5 | import os
6 | import uuid
7 | import zipfile
8 |
9 |
10 | def read(name: str) -> str:
11 | with open(os.path.join("expansions", name)) as infile:
12 | return infile.read()
13 |
14 |
15 | # fmt: off
16 | SNIPPETS = {
17 | "!bq": "