├── src ├── robots.txt ├── theme │ ├── style.scss │ └── specktre.png ├── _scss │ ├── _index.scss │ ├── _tags.scss │ ├── _print.scss │ ├── _figures.scss │ ├── _latex.scss │ ├── _main.scss │ ├── _post.scss │ ├── _functions.scss │ ├── _layout.scss │ ├── _aside.scss │ ├── _settings.scss │ ├── _footer.scss │ ├── _mixins.scss │ ├── _archive.scss │ ├── _code.scss │ ├── _text.scss │ └── _pygments.scss ├── analytics │ ├── a.gif │ └── a.js ├── images │ ├── twist-ties.jpg │ ├── vpc_networking.png │ ├── cant-login-to-aws.png │ ├── elasticsearch-after.png │ ├── partial_inventory.png │ ├── elasticsearch-before.png │ ├── overzealous-f-strings.png │ ├── azure-pipelines-library.jpg │ ├── azure-pipelines-secret-files.jpg │ └── azure-pipelines-permissions-error.jpg ├── _layouts │ ├── post.html │ ├── default.html │ ├── page.html │ └── compress.html ├── _includes │ ├── header.html │ ├── copyright_years.html │ ├── footer.html │ ├── socialgraph.html │ ├── head.html │ └── post_content.html ├── _github.json ├── _posts │ ├── 2016 │ │ └── 2016-12-01-why-does-hypothesis-try-the-same-example-three-times-before-failing.md │ ├── 2018 │ │ ├── 2018-10-31-where-to-find-the-tandem-vault-api-docs.md │ │ ├── 2018-11-23-use-to-jump-to-line-in-nano.md │ │ ├── 2018-11-02-how-to-bypass-tandem-vault-active-directory-login.md │ │ ├── 2018-10-28-when-partial-functions-and-currying-finally-clicked.md │ │ ├── 2018-10-31-linode-have-a-terraform-provider.md │ │ ├── 2018-11-27-licenses-supported-on-the-front-end-of-wellcomecollection-org.md │ │ ├── 2018-11-10-the-reusable-variant-of-cable-tie-is-twist-tie.md │ │ ├── 2018-11-05-create-compact-json-with-python.md │ │ ├── 2018-11-15-add-a-consistent-border-around-an-image.md │ │ ├── 2018-10-23-be-careful-how-much-logic-you-put-in-f-strings.md │ │ ├── 2018-11-13-create-a-tarball-of-files-in-git.md │ │ ├── 2018-11-14-use-a-unicode-dot-to-skip-twitter-s-link-replacement.md │ │ ├── 2018-10-24-preserve-double-dashes-with-smartypants.md │ │ ├── 2018-10-24-travelling-in-slovenia.md │ │ ├── 2018-11-14-running-as-root-inside-a-docker-container.md │ │ ├── 2018-11-27-how-to-check-for-substrings-in-scalatest.md │ │ ├── 2018-11-15-using-nginx-as-a-proxy-for-dynamic-hosts.md │ │ ├── 2018-11-05-boosting-an-individual-field-in-a-simple-query-string-query.md │ │ ├── 2018-11-05-experiments-with-the-linode-terraform-provider.md │ │ ├── 2018-11-12-replace-white-parts-of-an-image-with-transparency.md │ │ ├── 2018-11-05-delete-elasticsearch-indexes-to-improve-performance.md │ │ ├── 2018-10-25-rendering-markdown-without-p-tags-in-jekyll.md │ │ ├── 2018-04-10-standard-ia-in-s-incurs-a-minimum-day-charge.md │ │ ├── 2018-03-05-sharing-files-with-my-work-computer-using-dropbox.md │ │ ├── 2018-11-26-how-to-suppress-installing-rdoc-ri-docs-when-running-gem-install.md │ │ ├── 2018-10-24-using-xargs-for-parallel-processing.md │ │ ├── 2018-11-05-be-careful-of-assembling-partial-databases-with-concurrency.md │ │ ├── 2018-10-24-using-force-with-lease.md │ │ ├── 2018-10-28-replacing-map-flatten-with-flatmap.md │ │ ├── 2018-11-12-failing-to-find-an-implicit-objectstore-when-it-wants-an-executioncontext.md │ │ ├── 2018-11-28-beware-of-dynamic-arguments-in-apply-methods.md │ │ ├── 2018-11-15-notes-on-vpc-networking-and-acls.md │ │ ├── 2018-10-24-beware-ambiguous-dates-with-dateutil-parse.md │ │ ├── 2018-11-06-beware-of-using-val-in-abstract-traits.md │ │ ├── 2018-11-07-logging-into-the-aws-console-when-your-alias-isn-t-working.md │ │ ├── 2018-11-06-running-after-deleting-the-overlay-directory.md │ │ ├── 2018-09-17-list-all-git-object-ids-and-their-type.md │ │ ├── 2018-07-17-beware-the-order-of-inheritance-in-scala.md │ │ ├── 2018-11-10-some-notes-on-using-typesafe-for-config.md │ │ └── 2018-11-23-notes-from-working-through-the-ruby-koans.md │ └── 2019 │ │ ├── 2019-06-04-solving-s-accessdenied-when-calling-putobject-with-the-s-putobject-permission.md │ │ ├── 2019-05-08-java-parsing-an-s3-uri.md │ │ ├── 2019-07-01-deleting-nested-json-fields-with-circe-optics.md │ │ ├── 2019-05-07-scala-named-capturing-groups-in-a-regex.md │ │ ├── 2019-04-20-javascript-edit-the-url-in-the-window-without-reloading-the-page.md │ │ ├── 2019-04-20-python-use-the-python-magic-library-to-detect-the-mimetype-of-some-bytes.md │ │ ├── 2019-05-07-scala-iterating-over-the-newlines-of-an-inputstream.md │ │ ├── 2019-06-08-python-hashing-files-or-file-like-objects-efficiently.md │ │ ├── 2019-05-07-scala-convert-a-string-to-an-inputstream.md │ │ ├── 2019-05-23-sort-by-extname-basename-dirname-to-reduce-the-size-of-compressed-streams.md │ │ ├── 2019-06-04-latency-issues-with-nlb-and-ecs-tasks.md │ │ ├── 2019-05-09-github-set-co-commit-credit-with-co-authored-by.md │ │ ├── 2019-05-12-is-apache-using-threaded-mpms-or-pre-fork.md │ │ ├── 2019-08-28-installing-mimetype-on-alpine-linux.md │ │ ├── 2019-04-20-getting-the-cover-of-an-epub-file.md │ │ ├── 2019-05-11-how-do-dreamwidth-post-ids-increment.md │ │ ├── 2019-04-20-http-the-content-disposition-header.md │ │ ├── 2019-05-11-hashes-and-hash-references.md │ │ ├── 2019-08-14-condition-parameter-type-does-not-match-schema-type.md │ │ ├── 2019-05-15-getting-the-latest-version-of-a-range-key.md │ │ ├── 2019-04-20-javascript-manipulating-url-query-parameters.md │ │ ├── 2019-05-09-jekyll-creating-permalinks-to-posts.md │ │ ├── 2019-04-20-python-include-the-filename-content-type-and-content-length-in-a-requests-upload.md │ │ ├── 2019-04-20-python-use-the-whitenoise-library-to-serve-static-files.md │ │ ├── 2019-06-02-secret-files-in-azure-pipelines.md │ │ └── 2019-05-09-scanamo-conditional-updates-on-nested-fields.md ├── 404.md ├── _plugins │ ├── markdown.rb │ ├── fix_footnotes.rb │ ├── cleanup_text.rb │ ├── escape_email.rb │ ├── github.rb │ ├── tag_cloud.rb │ └── theming.rb ├── 410.md ├── index.md └── static │ └── notebook.js ├── .gitignore ├── id_rsa.enc ├── screenshot.png ├── .travis.yml ├── tag_tally.sh ├── id_rsa.pub ├── _config.yml ├── travis_run.sh ├── create_post.rb ├── README.md ├── LICENSE └── Makefile /src/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: 3 | -------------------------------------------------------------------------------- /src/theme/style.scss: -------------------------------------------------------------------------------- 1 | --- 2 | --- 3 | 4 | @import "_main.scss" 5 | -------------------------------------------------------------------------------- /src/_scss/_index.scss: -------------------------------------------------------------------------------- 1 | .index__title { 2 | padding-bottom: 5px; 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .sass-cache 3 | _site 4 | .jekyll-metadata 5 | 6 | id_rsa 7 | -------------------------------------------------------------------------------- /id_rsa.enc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexwlchan/notebook.alexwlchan.net/live/id_rsa.enc -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexwlchan/notebook.alexwlchan.net/live/screenshot.png -------------------------------------------------------------------------------- /src/analytics/a.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexwlchan/notebook.alexwlchan.net/live/src/analytics/a.gif -------------------------------------------------------------------------------- /src/theme/specktre.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexwlchan/notebook.alexwlchan.net/live/src/theme/specktre.png -------------------------------------------------------------------------------- /src/images/twist-ties.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexwlchan/notebook.alexwlchan.net/live/src/images/twist-ties.jpg -------------------------------------------------------------------------------- /src/_layouts/post.html: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | --- 4 | 5 | {% assign post = page %} 6 | {% include post_content.html %} 7 | -------------------------------------------------------------------------------- /src/images/vpc_networking.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexwlchan/notebook.alexwlchan.net/live/src/images/vpc_networking.png -------------------------------------------------------------------------------- /src/images/cant-login-to-aws.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexwlchan/notebook.alexwlchan.net/live/src/images/cant-login-to-aws.png -------------------------------------------------------------------------------- /src/images/elasticsearch-after.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexwlchan/notebook.alexwlchan.net/live/src/images/elasticsearch-after.png -------------------------------------------------------------------------------- /src/images/partial_inventory.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexwlchan/notebook.alexwlchan.net/live/src/images/partial_inventory.png -------------------------------------------------------------------------------- /src/images/elasticsearch-before.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexwlchan/notebook.alexwlchan.net/live/src/images/elasticsearch-before.png -------------------------------------------------------------------------------- /src/images/overzealous-f-strings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexwlchan/notebook.alexwlchan.net/live/src/images/overzealous-f-strings.png -------------------------------------------------------------------------------- /src/images/azure-pipelines-library.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexwlchan/notebook.alexwlchan.net/live/src/images/azure-pipelines-library.jpg -------------------------------------------------------------------------------- /src/images/azure-pipelines-secret-files.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexwlchan/notebook.alexwlchan.net/live/src/images/azure-pipelines-secret-files.jpg -------------------------------------------------------------------------------- /src/_includes/header.html: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /src/images/azure-pipelines-permissions-error.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexwlchan/notebook.alexwlchan.net/live/src/images/azure-pipelines-permissions-error.jpg -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: sh 2 | 3 | sudo: required 4 | 5 | services: 6 | - docker 7 | 8 | branches: 9 | only: 10 | - "live" 11 | 12 | script: 13 | - ./travis_run.sh 14 | -------------------------------------------------------------------------------- /src/_scss/_tags.scss: -------------------------------------------------------------------------------- 1 | // Styles for the /tags page 2 | 3 | .tags__cloud { 4 | text-align: justify; 5 | 6 | a:not(:last-child) { 7 | margin-right: 0.15em; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /tag_tally.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -o errexit 4 | set -o nounset 5 | 6 | grep --no-filename --recursive 'tags: ' src/_posts | cut -d ':' -f2- | tr ' ' '\n' | grep -v -e '^$' | sort | uniq -c | sort 7 | -------------------------------------------------------------------------------- /src/_github.json: -------------------------------------------------------------------------------- 1 | { 2 | "scanamo/scanamo#136": { 3 | "title": "Support for nested fields in conditional expressions" 4 | }, 5 | "wellcometrust/platform#2967": { 6 | "title": "Archive services config cleanup " 7 | } 8 | } -------------------------------------------------------------------------------- /src/_scss/_print.scss: -------------------------------------------------------------------------------- 1 | // via https://medium.com/@matuzo/writing-css-with-accessibility-in-mind-8514a0007939 2 | @media print { 3 | a[href^="http"]:not([href*="alexwlchan.net"])::after { 4 | content: " (" attr(href) ")"; 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/_scss/_figures.scss: -------------------------------------------------------------------------------- 1 | figure { 2 | @include centred(100%); 3 | } 4 | 5 | figcaption { 6 | &, a, a:visited { 7 | color: $accent-grey; 8 | } 9 | font-size: $meta-font-size; 10 | margin-top: 8px; 11 | line-height: $meta-line-height; 12 | } 13 | -------------------------------------------------------------------------------- /src/_posts/2018/2018-10-31-where-to-find-the-tandem-vault-api-docs.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: post 3 | title: Where to find the Tandem Vault API docs 4 | date: 2018-10-31 11:44:00 +0000 5 | tags: tandem-vault 6 | --- 7 | 8 | The Tandem Vault API docs can be found at . 9 | -------------------------------------------------------------------------------- /src/_posts/2018/2018-11-23-use-to-jump-to-line-in-nano.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: post 3 | title: Use ^_ to jump to line in nano 4 | date: 2018-11-23 11:54:29 +0000 5 | tags: nano 6 | --- 7 | 8 | Which, to be fair, is in the help text it gives you. 9 | I'd just never even realised nano could to that! 10 | -------------------------------------------------------------------------------- /src/404.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: page 3 | title: 404 Not Found 4 | --- 5 | 6 | **This page wasn't found.** 7 | 8 | If you were expecting to see something here, please [send me a tweet](https://twitter.com/{{ site.social.twitter }}) or [drop me an email]({{ site.email|encode_mailto }}?subject=404 Not Found on alexwlchan.net). 9 | -------------------------------------------------------------------------------- /src/_posts/2018/2018-11-02-how-to-bypass-tandem-vault-active-directory-login.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: post 3 | title: How to bypass Tandem Vault Active Directory login 4 | date: 2018-11-02 12:13:34 +0000 5 | tags: tandem-vault 6 | --- 7 | 8 | To log in to Tandem Vault, and bypass Active Directory login, go to . 9 | -------------------------------------------------------------------------------- /src/_layouts/default.html: -------------------------------------------------------------------------------- 1 | --- 2 | layout: compress 3 | --- 4 | 5 | 6 | 7 | 8 | {% include head.html %} 9 | 10 | 11 | {% include header.html %} 12 | 13 | {{ content | cleanup_text | fix_footnote }} 14 | 15 | {% include footer.html %} 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/analytics/a.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | if (navigator.doNotTrack == 1) { 3 | console.log("You have Do Not Track enabled, so I don't record any analytics."); 4 | } else { 5 | var doc = document, enc = encodeURIComponent, img = new Image; 6 | img.src = "/analytics/a.gif?url=" + enc(doc.location.href) + "&ref=" + enc(doc.referrer) + "&t=" + enc(doc.title); 7 | } 8 | })() 9 | -------------------------------------------------------------------------------- /src/_plugins/markdown.rb: -------------------------------------------------------------------------------- 1 | module Jekyll 2 | module MarkdownFilter 3 | def render_markdown(input) 4 | site = @context.registers[:site] 5 | converter = site.find_converter_instance(::Jekyll::Converters::Markdown) 6 | converter.convert(input).sub("

", "").sub("

", "") 7 | end 8 | end 9 | end 10 | 11 | Liquid::Template::register_filter(Jekyll::MarkdownFilter) 12 | -------------------------------------------------------------------------------- /id_rsa.pub: -------------------------------------------------------------------------------- 1 | ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDTZNGmzJN+9jDIydpjc34ppMHMwYU2kN2bJXcCHOj1qPGaFeYqsrJX4cnLI4uZBNsTpx3VRrzAS+J3m+EKTZQ524xKCfH3TBB0FdqcmPNyi5RTzUhSDUdwFosYQpTWxsndBD2rhHcVNy3uB6E/52vm0VoCyA5B8Ch1E9+/T/xDyhzN5m+7eJDeyw3oSHkjkih/8IhUSLrf1fmpTMnsbTi/HGQocXN0bM1GuTxatpWefLCSYcQzugSoxmQmL0WFM3brJzNgELIWeK5oNX+0o5h0HNdKF4K9YBs2ywjI7rAPs498mEB7d0NZYbqBWH8XCuSCwhYt2YFXYY5cKKK4Cp3T chana@01123-COMMS 2 | -------------------------------------------------------------------------------- /src/_posts/2018/2018-10-28-when-partial-functions-and-currying-finally-clicked.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: post 3 | title: When partial functions and currying finally "clicked" 4 | date: 2018-10-28 18:25:00 +0000 5 | tags: scala functional-programming 6 | --- 7 | 8 | This is the patch where currying and partial functions finally "clicked": [wellcometrust/platform#2900](https://github.com/wellcometrust/platform/pull/2900). 9 | -------------------------------------------------------------------------------- /src/_posts/2019/2019-06-04-solving-s-accessdenied-when-calling-putobject-with-the-s-putobject-permission.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: post 3 | title: "Solving 'S3: AccessDenied' when calling PutObject with the s3:PutObject permission" 4 | date: 2019-06-04 19:53:51 +0100 5 | tags: aws aws:s3 aws:iam 6 | --- 7 | 8 | Have you tried adding `s3:PutObjectAcl` to your IAM policy document? See 9 | -------------------------------------------------------------------------------- /src/_posts/2018/2018-10-31-linode-have-a-terraform-provider.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: post 3 | title: Linode have a Terraform provider 4 | date: 2018-10-31 10:03:00 +0000 5 | tags: linode terraform 6 | --- 7 | 8 | Linode just announced [their Terraform provider](https://blog.linode.com/2018/10/30/now-available-linode-terraform-provider/). 9 | 10 | I haven't used it yet, but I should try next time I'm fiddling with my instance. 11 | 12 | -------------------------------------------------------------------------------- /src/_posts/2019/2019-05-08-java-parsing-an-s3-uri.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: post 3 | title: "Java: parsing an S3 URI" 4 | date: 2019-05-08 12:00:23 +0100 5 | tags: java scala aws 6 | --- 7 | 8 | ```java 9 | import java.net.URI 10 | 11 | val loc = new URI("s3://bucket-name/path/to/key.txt") 12 | 13 | println(loc.getScheme) // "s3" 14 | println(loc.getHost) // "bucket-name" 15 | println(loc.getPath) // "path/to/key.txt" 16 | ``` 17 | -------------------------------------------------------------------------------- /src/_posts/2019/2019-07-01-deleting-nested-json-fields-with-circe-optics.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: post 3 | title: "Deleting nested JSON fields with Circe optics" 4 | date: 2019-07-01 18:30:47 +0100 5 | tags: scala 6 | --- 7 | 8 | Removing a top-level field: 9 | 10 | ```scala 11 | root.obj.modify { _.remove("ingestType") }(json) 12 | ``` 13 | 14 | Removing a nested field: 15 | 16 | ```scala 17 | root.ingestType.obj.modify { _.remove("x") }(json) 18 | ``` 19 | -------------------------------------------------------------------------------- /src/_posts/2016/2016-12-01-why-does-hypothesis-try-the-same-example-three-times-before-failing.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: post 3 | title: Why does Hypothesis try the same example three times before failing? 4 | date: 2016-12-01 21:50:00 +0000 5 | tags: hypothesis 6 | --- 7 | 8 | From the #hypothesis IRC channel: 9 | 10 | * Once to find the failure 11 | * Once to check the failure isn’t flakey 12 | * Once to create a failure which is spotted by the test runner 13 | -------------------------------------------------------------------------------- /src/_posts/2018/2018-11-27-licenses-supported-on-the-front-end-of-wellcomecollection-org.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: post 3 | title: Licenses supported on the front-end of wellcomecollection.org 4 | date: 2018-11-27 17:07:42 +0000 5 | tags: wellcome 6 | --- 7 | 8 | I've had to find the file with license definitions several times, so here's an easier-to-find link: 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/_plugins/fix_footnotes.rb: -------------------------------------------------------------------------------- 1 | # Make sure that footnote markers are rendered as a text 2 | # arrow on iOS devices, not emoji. For more info: 3 | # http://daringfireball.net/linked/2015/04/22/unicode-emoji 4 | 5 | module Jekyll 6 | module FootnoteFilter 7 | def fix_footnote(input) 8 | input.gsub("↩", "↩︎").gsub("↩", "↩︎") 9 | end 10 | end 11 | end 12 | 13 | Liquid::Template::register_filter(Jekyll::FootnoteFilter) 14 | -------------------------------------------------------------------------------- /src/_posts/2018/2018-11-10-the-reusable-variant-of-cable-tie-is-twist-tie.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: post 3 | title: The reusable variant of cable tie is "twist tie" 4 | date: 2018-11-10 18:06:43 +0000 5 | tags: cable-management 6 | --- 7 | 8 | I saw a whole pot of these at a friend's house when they were tidying their box of random cables. 9 | 10 | ![](/images/twist-ties.jpg) 11 | 12 | It took me a while to find out what to Google -- the phrase you want is "twist tie". 13 | -------------------------------------------------------------------------------- /src/_posts/2019/2019-05-07-scala-named-capturing-groups-in-a-regex.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: post 3 | title: "Scala: Named capturing groups in a regex" 4 | date: 2019-05-07 17:37:38 +0100 5 | tags: scala snippets 6 | --- 7 | 8 | ```scala 9 | import scala.util.matching.Regex 10 | 11 | val r: Regex = new Regex("([a-z]*) ([0-9]*)", "name", "number") 12 | 13 | val m = r.findFirstMatchIn("lexie 25").get 14 | 15 | println(m.group("name")) // "lexie" 16 | println(m.group("number")) // 25 17 | ``` 18 | -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | title: "alexwlchan’s notebook" 2 | email: "alex@alexwlchan.net" 3 | url: "https://notebook.alexwlchan.net" 4 | 5 | source: "src" 6 | destination: "_site" 7 | 8 | social: 9 | github: "alexwlchan" 10 | twitter: "alexwlchan" 11 | linode_referral: "ba2e6ce21e0c63952a7c74967ea0b96617bd44a3" 12 | 13 | date_format: "%-d %B %Y" 14 | 15 | permalink: "/:year/:month/:title/" 16 | 17 | liquid: 18 | error_mode: "strict" 19 | 20 | sass: 21 | sass_dir: "_scss" 22 | style: "compressed" 23 | -------------------------------------------------------------------------------- /src/_posts/2018/2018-11-05-create-compact-json-with-python.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: post 3 | title: Create compact JSON with Python 4 | date: 2018-10-24 08:18:00 +0000 5 | tags: python json 6 | --- 7 | 8 | To create compact JSON in Python: 9 | 10 | ```pycon 11 | >>> json.dumps({'a':1, 'b':2}) 12 | '{"a": 1, "b": 2}' 13 | 14 | >>> json.dumps({'a':1, 'b':2}, separators=(',',':')) 15 | '{"a":1,"b":2}' 16 | ``` 17 | 18 | (via [Raymond Hettinger](https://twitter.com/raymondh/status/842777864193769472)) 19 | -------------------------------------------------------------------------------- /src/_posts/2018/2018-11-15-add-a-consistent-border-around-an-image.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: post 3 | title: Add a consistent border around an image 4 | date: 2018-11-15 13:04:03 +0000 5 | tags: imagemagick 6 | --- 7 | 8 | This command removes all the empty whitespace around an image, then adds a consistent border to what's left: 9 | 10 | ```shell 11 | convert \ 12 | -trim "$FILENAME" \ 13 | -bordercolor white -border 50x50 \ 14 | "$OUTFILE" 15 | ``` 16 | 17 | Useful for making diagrams and so on. 18 | -------------------------------------------------------------------------------- /src/_posts/2018/2018-10-23-be-careful-how-much-logic-you-put-in-f-strings.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: post 3 | title: Be careful how much logic you put in f-strings 4 | date: 2018-10-23 09:12:00 +0000 5 | tags: python 6 | --- 7 | 8 | As witnessed here: 9 | 10 | ![](/images/overzealous-f-strings.png) 11 | 12 | I'd been trying to lowercase the ".JPG" file extension, but I got the `.lower()` outside the braces. 13 | The f-string was sufficiently complex for me not to notice -- next time, handle that separately. 14 | -------------------------------------------------------------------------------- /src/410.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: page 3 | title: 410 Gone 4 | --- 5 | 6 | **There used to be something here, but I've deliberately removed it.** 7 | 8 | You may have followed an old link, or saved this page in the past --- whatever the case, it's not here any more. 9 | Old content may be accessible through [the Internet Archive](https://archive.org/). 10 | 11 | If you want to see what used to be here, or ask me why I removed it, you can [send me an email]({{ site.email|encode_mailto }}?subject=410 Gone on notebook.alexwlchan.net). 12 | -------------------------------------------------------------------------------- /src/_posts/2018/2018-11-13-create-a-tarball-of-files-in-git.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: post 3 | title: Create a tarball of files from a directory in Git 4 | date: 2016-11-27 10:17:00 +0000 5 | tags: git 6 | --- 7 | 8 | "Create an archive of files from a named tree" ~ 9 | 10 | Discovered at work while we were doing the Git migration: you can use this to get a tarball of the repository in a particular state, or of certain subsets of the repository. 11 | Useful for passing around subsets of a repo? 12 | -------------------------------------------------------------------------------- /src/_posts/2019/2019-04-20-javascript-edit-the-url-in-the-window-without-reloading-the-page.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: post 3 | title: "JavaScript: Edit the URL in the window without reloading the page" 4 | date: 2019-04-20 21:24:30 +0100 5 | tags: javascript 6 | --- 7 | 8 | Yes, that's possible: 9 | 10 | ```javascript 11 | window.history.pushState({path: newUrl }, "", newUrl); 12 | ``` 13 | 14 | You can also use `replaceState`, which overwrites the current history entry. 15 | (I can't spot the difference in behaviour in Safari.) 16 | -------------------------------------------------------------------------------- /src/_scss/_latex.scss: -------------------------------------------------------------------------------- 1 | // https://stackoverflow.com/a/8160532/1558022 2 | .tex sub, .latex sub, .latex sup { 3 | text-transform: uppercase; 4 | top: .1ex; 5 | line-height:0.1em; 6 | } 7 | 8 | .tex sub, .latex sub { 9 | vertical-align: -0.5ex; 10 | margin-left: -0.1667em; 11 | margin-right: -0.125em; 12 | } 13 | 14 | .tex, .latex, .tex sub, .latex sub { 15 | font-size: 1em; 16 | } 17 | 18 | .latex sup { 19 | font-size: .77em; 20 | vertical-align: 0.25em; 21 | margin-left: -0.36em; 22 | margin-right: -0.15em; 23 | } 24 | -------------------------------------------------------------------------------- /src/_includes/copyright_years.html: -------------------------------------------------------------------------------- 1 | {% assign first_year = "2018" %} 2 | 3 | {% assign curr_year = site.time | date: "%Y" %} 4 | {% if first_year == curr_year %} 5 | {{ first_year }} 6 | {% else %} 7 | {% assign first_year_prefix = first_year | slice: 0, 2 %} 8 | {% assign curr_year_prefix = curr_year | slice: 0, 2 %} 9 | 10 | {% if first_year_prefix == curr_year_prefix %} 11 | {{ first_year }}–{{ site.time | date: "%y" }} 12 | {% else %} 13 | {{ first_year }}–{{ site.time | date: "%Y" }} 14 | {% endif %} 15 | {% endif %} 16 | -------------------------------------------------------------------------------- /src/_scss/_main.scss: -------------------------------------------------------------------------------- 1 | @import "_settings.scss"; 2 | @import "_functions.scss"; 3 | @import "_mixins.scss"; 4 | 5 | @import "_layout.scss"; 6 | @import "_text.scss"; 7 | 8 | // Major page components 9 | @import "_aside.scss"; 10 | @import "_footer.scss"; 11 | 12 | // Template-specific styles 13 | @import "_archive.scss"; 14 | @import "_index.scss"; 15 | @import "_post.scss"; 16 | 17 | // Minor/optional page components 18 | @import "_code.scss"; 19 | @import "_pygments.scss"; 20 | @import "_latex.scss"; 21 | @import "_figures.scss"; 22 | @import "_tags.scss"; 23 | -------------------------------------------------------------------------------- /src/_scss/_post.scss: -------------------------------------------------------------------------------- 1 | .continue_reading { 2 | font-weight: bold; 3 | &::after { 4 | content: "→"; 5 | } 6 | } 7 | 8 | .post__separator { 9 | color: $light-grey; 10 | text-align: center; 11 | font-size: 2em; 12 | margin-top: 1em; 13 | margin-bottom: 1em; 14 | } 15 | 16 | .post__meta, .page__meta { 17 | line-height: 1.45em; 18 | &, a, a:visited { 19 | color: $accent-grey; 20 | 21 | } 22 | 23 | font-size: $meta-font-size; 24 | .post__permalink a { 25 | &, &:visited { 26 | color: $primary-color; 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/_posts/2018/2018-11-14-use-a-unicode-dot-to-skip-twitter-s-link-replacement.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: post 3 | title: Use a Unicode dot to skip Twitter's link replacement 4 | date: 2018-11-14 07:13:41 +0000 5 | tags: twitter unicode 6 | --- 7 | 8 | [Ned Batchelder](https://twitter.com/nedbat/status/1062371519370801152): 9 | 10 | > Unicode twitter geekiness: if I say [coverage.py](http://coverage.py) in a tweet, it gets linked as a URL (which doesn’t exist). So instead I say coverage․py, using U+2024 “ONE DOT LEADER” instead: http://www.fileformat.info/info/unicode/char/2024/index.htm 11 | -------------------------------------------------------------------------------- /src/_posts/2019/2019-04-20-python-use-the-python-magic-library-to-detect-the-mimetype-of-some-bytes.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: post 3 | title: "Python: Use the python-magic library to detect the mimetype of some bytes" 4 | date: 2019-04-20 21:24:30 +0100 5 | tags: python 6 | --- 7 | 8 | [python-magic] is a library for detecting the mimetype of some bytes. 9 | A simple example: 10 | 11 | ```python 12 | import magic 13 | 14 | assert isinstance(data, bytes) 15 | guessed_mimetype = magic.from_buffer(data, mime=True) 16 | ``` 17 | 18 | [python-magic]: https://pypi.org/project/python-magic/ 19 | -------------------------------------------------------------------------------- /src/_posts/2019/2019-05-07-scala-iterating-over-the-newlines-of-an-inputstream.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: post 3 | title: "Scala: Iterating over the newlines of an InputStream" 4 | date: 2019-05-07 17:38:01 +0100 5 | tags: scala snippets 6 | --- 7 | 8 | ```scala 9 | import java.io.{BufferedReader, InputStream, InputStreamReader} 10 | 11 | val is = new InputStream(…) 12 | 13 | val bufferedReader = new BufferedReader(new InputStreamReader(is)) 14 | 15 | Iterator 16 | .continually(bufferedReader.readLine()) 17 | .takeWhile { _ != null } 18 | .foreach { line => println(line) } 19 | ``` 20 | -------------------------------------------------------------------------------- /src/_posts/2018/2018-10-24-preserve-double-dashes-with-smartypants.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: post 3 | title: Preserve double dashes with Smartypants 4 | date: 2018-10-24 21:51:00 +0000 5 | tags: markdown smartypants 6 | --- 7 | 8 | If you want to write a double dash (`--`) in a Markdown document, and not have Smartypants turn it into an en dash (`–`), add a [zero-width space](https://en.wikipedia.org/wiki/Zero-width_space) to the command: 9 | 10 | ``` 11 | -​-force-with-lease 12 | --force-with-lease 13 | ``` 14 | 15 | Compare: 16 | 17 | > -​-force-with-lease
18 | > --force-with-lease 19 | -------------------------------------------------------------------------------- /travis_run.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -o errexit 4 | set -o nounset 5 | 6 | echo "*** Attempting to build the site" 7 | mkdir -p _site 8 | make build 9 | 10 | if [[ "$TRAVIS_EVENT_TYPE" == "pull_request" ]] 11 | then 12 | echo "*** Pull request, skipping deploy" 13 | exit 0 14 | fi 15 | 16 | echo "*** Loading Travis RSA key" 17 | openssl aes-256-cbc \ 18 | -K $encrypted_83630750896a_key \ 19 | -iv $encrypted_83630750896a_iv \ 20 | -in id_rsa.enc \ 21 | -out id_rsa -d 22 | 23 | chmod 400 id_rsa 24 | mv id_rsa ~/.ssh/id_rsa 25 | 26 | echo "*** Uploading published site to Linode" 27 | make deploy 28 | -------------------------------------------------------------------------------- /src/_posts/2018/2018-10-24-travelling-in-slovenia.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: post 3 | title: Travelling in Slovenia 4 | date: 2018-10-24 07:15:00 +0000 5 | tags: travel slovenia 6 | --- 7 | 8 | [Local laws and customs](https://www.gov.uk/foreign-travel-advice/slovenia/local-laws-and-customs): 9 | 10 | > Carry a copy of your passport at all times as a form of identification. 11 | > 12 | > All foreign nationals visiting Slovenia must register with the Police within 3 days of arrival or risk paying a fine. […] 13 | > 14 | > There are heavy on-the-spot fines for jaywalking. You should only cross the road at designated crossing points. 15 | -------------------------------------------------------------------------------- /src/_posts/2019/2019-06-08-python-hashing-files-or-file-like-objects-efficiently.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: post 3 | title: "Python: hashing files or file-like objects efficiently" 4 | date: 2019-06-08 09:07:13 +0100 5 | tags: snippets python 6 | --- 7 | 8 | Don't load the whole file into memory; load it a chunk at a time. 9 | Snippet: 10 | 11 | ```python 12 | import hashlib 13 | 14 | 15 | def hash_file_ALGORITHM(f, block_size=65536): 16 | h = hashlib.HASHING_ALGORITHM() 17 | while True: 18 | buf = f.read(block_size) 19 | if not buf: 20 | break 21 | h.update(buf) 22 | return h 23 | ``` 24 | -------------------------------------------------------------------------------- /src/_posts/2018/2018-11-14-running-as-root-inside-a-docker-container.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: post 3 | title: Running as root inside a Docker container 4 | date: 2018-11-14 18:02:55 +0000 5 | tags: docker 6 | --- 7 | 8 | If you need to get a root shell inside a Docker container, and the default user isn't root and `sudo` isn't available, you can use the `--user=0` flag: 9 | 10 | ```console 11 | $ docker exec --user 0 --interactive --tty CONTAINER sh 12 | $ docker exec -u 0 -it CONTAINER sh 13 | ``` 14 | 15 | Discovered when trying to install packages inside a container, but we didn't have sudo or apt-get privileges, and sudo wasn't installed. 16 | -------------------------------------------------------------------------------- /src/_posts/2018/2018-11-27-how-to-check-for-substrings-in-scalatest.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: post 3 | title: How to check for substrings in scalatest 4 | date: 2018-11-27 11:13:02 +0000 5 | tags: scala 6 | --- 7 | 8 | I feel like I have to look this up on a regular basis, so here's a quick reminder of the assert pattern you use: 9 | 10 | ```scala 11 | class StringTest extends FunSpec with Matchers { 12 | it("recognises substrings") { 13 | "foo bar baz" should include("foo") 14 | "foo bar baz" should include("bar") 15 | "foo bar baz" should include("baz") 16 | 17 | "foo bar baz" should not include("hello") 18 | } 19 | } 20 | ``` 21 | -------------------------------------------------------------------------------- /src/_posts/2018/2018-11-15-using-nginx-as-a-proxy-for-dynamic-hosts.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: post 3 | title: Using nginx as a proxy for dynamic hosts 4 | date: 2018-11-15 13:09:22 +0000 5 | tags: nginx networking 6 | --- 7 | 8 | If you're using nginx as a proxy for a backend service, beware that nginx may only resolve the IP address once -- and if the backend moves, you have problems. 9 | 10 | You can do dynamic variables in nginx (apparently), give backend services a static IP address, or move the problem elsewhere. 11 | In practice we did this by running nginx+backend in the same ECS task definition, so we can use the proxy name and not worry about the networking ourselves. 12 | -------------------------------------------------------------------------------- /src/_posts/2019/2019-05-07-scala-convert-a-string-to-an-inputstream.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: post 3 | title: "Scala: Convert a string to an InputStream and back" 4 | date: 2019-05-07 17:36:43 +0100 5 | tags: scala snippets 6 | --- 7 | 8 | String to stream: 9 | 10 | ```scala 11 | import java.io.InputStream 12 | 13 | import org.apache.commons.io.IOUtils 14 | 15 | def toInputStream(s: String): InputStream = 16 | IOUtils.toInputStream(s, "UTF-8") 17 | ``` 18 | 19 | Stream to string: 20 | 21 | ```scala 22 | import java.io.InputStream 23 | 24 | import scala.io.Source 25 | 26 | def fromInputStream(is: InputStream): String = 27 | Source.fromInputStream(is).mkString 28 | ``` 29 | -------------------------------------------------------------------------------- /src/_scss/_functions.scss: -------------------------------------------------------------------------------- 1 | /// Convert a decimal number to a two-digit hex string. 2 | @function decToHex($dec) { 3 | $hex: "0123456789ABCDEF"; 4 | $first: (($dec - $dec % 16)/16) + 1; 5 | $second: ($dec % 16) + 1; 6 | @return str-slice($hex, $first, $first) + str-slice($hex, $second, $second); 7 | } 8 | 9 | /// Convert a color to a hex string, omitting the leading hash. 10 | /// 11 | /// For example, #d009dc would become 'd009dc'. 12 | @function colorToHexString($color) { 13 | $red_hex: decToHex(red($color)); 14 | $green_hex: decToHex(green($color)); 15 | $blue_hex: decToHex(blue($color)); 16 | @return to-lower-case('#{$red_hex}#{$green_hex}#{$blue_hex}'); 17 | } 18 | -------------------------------------------------------------------------------- /src/_layouts/page.html: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | --- 4 |
5 |

6 | {{ page.title | smartify | escape }} 7 |

8 | 9 | {% if page.last_updated or page.meta %} 10 |
11 |
    12 | {% if page.last_updated %} 13 |
  • Last updated {{ page.last_updated | date: site.date_format }}
  • 14 | {% endif %} 15 | {% for m in page.meta %} 16 |
  • {{ m }}
  • 17 | {% endfor %} 18 |
19 |
20 | {% endif %} 21 | 22 | {{ content | cleanup_text | smartify }} 23 |
24 | -------------------------------------------------------------------------------- /src/_posts/2018/2018-11-05-boosting-an-individual-field-in-a-simple-query-string-query.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: post 3 | title: Boosting an individual field in a Simple Query String query 4 | date: 2018-11-05 16:12:23 +0000 5 | tags: elasticsearch 6 | --- 7 | 8 | By default, a Simple Query String query will search all fields (`*`). 9 | 10 | If you want to preserve that behaviour but just boost a couple of fields beyond the rest, the following query seems to do it: 11 | 12 | ```json 13 | { 14 | "query": { 15 | "simple_query_string": { 16 | "fields": ["description^10", "title^5", ".*"], 17 | "query": "legs", 18 | "default_operator": "and" 19 | } 20 | } 21 | } 22 | ``` 23 | -------------------------------------------------------------------------------- /src/_posts/2019/2019-05-23-sort-by-extname-basename-dirname-to-reduce-the-size-of-compressed-streams.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: post 3 | title: "Sort by extname/basename/dirname to reduce the size of compressed streams" 4 | date: 2019-05-23 07:17:33 +0100 5 | tags: compression git 6 | --- 7 | 8 | Chris Dickson on Twitter: 9 | 10 | > I got to use a trick I learned from git today: if you're going to throw a directory full of files into a compressed stream, wait, take a second, 11 | > 12 | > sort those files by extname -> basename -> dirname first so files that are likely to be similar end up next to each other 13 | > 14 | > I just decreased the size of my compressed stream by 10% using this one weird trick 15 | -------------------------------------------------------------------------------- /src/_posts/2019/2019-06-04-latency-issues-with-nlb-and-ecs-tasks.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: post 3 | title: "Latency issues with NLB and ECS tasks" 4 | date: 2019-06-04 19:52:55 +0100 5 | tags: aws 6 | --- 7 | 8 | If the number of ECS tasks is less than the number of AZs served by an NLB, you get latent issues. 9 | 10 | See [Register Targets with your Target Group](https://docs.aws.amazon.com/elasticloadbalancing/latest/network/target-group-register-targets.html): 11 | 12 | > You register your targets with one or more target groups. Each target group must have at least one registered target in each Availability Zone that is enabled for the load balancer. You can register targets by instance ID or by IP address. 13 | -------------------------------------------------------------------------------- /src/_posts/2018/2018-11-05-experiments-with-the-linode-terraform-provider.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: post 3 | title: Experiments with the Linode Terraform provider 4 | date: 2018-11-05 21:23:17 +0000 5 | tags: linode terraform 6 | --- 7 | 8 | The docs don't yet have a list of instance types/regions (see [terraform-providers/terraform-provider-linode#7](https://github.com/terraform-providers/terraform-provider-linode/issues/7)); this is what I've managed to work out by guessing: 9 | 10 | * The London, UK is `eu-west` 11 | * The Linode 1GB plan is `g6-nanode-1` 12 | * The Linode 2GB plan is `g6-standard-1` 13 | 14 | I'm hoping they'll add more to the docs at some point, but those are the values most useful to me for now. 15 | -------------------------------------------------------------------------------- /src/_posts/2018/2018-11-12-replace-white-parts-of-an-image-with-transparency.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: post 3 | title: "ImageMagick: Replace white parts of an image with transparency" 4 | date: 2018-11-12 09:09:23 +0000 5 | tags: imagemagick 6 | --- 7 | 8 | Replace white sections of an image with transparency: 9 | 10 | ```console 11 | $ convert myimage.jpg -transparent white myimage.png 12 | ``` 13 | 14 | If it's not pure white, and you need a bit of extra: 15 | 16 | ```console 17 | $ convert myimage.jpg -fuzz 10% -transparent white myimage.png 18 | ``` 19 | 20 | If you want to additionally crop the image to the nontransparent portion: 21 | 22 | ```console 23 | $ convert myimage.jpg -fuzz 10% -transparent white -trim myimage.png 24 | ``` 25 | -------------------------------------------------------------------------------- /src/_scss/_layout.scss: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | padding: 0; 4 | } 5 | 6 | article { 7 | @include central_element(); 8 | padding-top: 3px; 9 | padding-bottom: 15px; 10 | } 11 | 12 | .dot_list { 13 | list-style-type: none; 14 | padding-left: 0px !important; 15 | 16 | // Ensure they all display in a line 17 | li { 18 | display: inline; 19 | &:not(:first-child)::before { 20 | content: " · "; 21 | } 22 | } 23 | } 24 | 25 | hr { 26 | background-color: $light-grey; 27 | height: 1px; 28 | border: 0px; 29 | } 30 | 31 | // Images never expand beyond the article bounds, and are centred when 32 | // they're too small. 33 | img, video { 34 | @include centred(100%); 35 | display: block; 36 | } 37 | -------------------------------------------------------------------------------- /src/_posts/2018/2018-11-05-delete-elasticsearch-indexes-to-improve-performance.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: post 3 | title: Delete Elasticsearch indexes to improve performance 4 | date: 2018-11-05 10:15:19 +0000 5 | tags: elasticsearch 6 | --- 7 | 8 | If your Elasticsearch cluster is having performance problems (visible through requests timing out under load), and against the limits of CPU and memory, try deleting unused indexes. 9 | We had a cluster with ~70 indexes, and deleting a bunch of indexes we weren't using made a noticeable difference. 10 | 11 | Before: 12 | 13 | ![](/images/elasticsearch-before.png) 14 | 15 | After: 16 | 17 | ![](/images/elasticsearch-after.png) 18 | 19 | (Screenshots from the Performance tab of the Elastic Cloud console.) 20 | -------------------------------------------------------------------------------- /src/_posts/2018/2018-10-25-rendering-markdown-without-p-tags-in-jekyll.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: post 3 | title: "Rendering Markdown without <p> tags in Jekyll" 4 | date: 2018-10-25 07:54:00 +0000 5 | tags: markdown jekyll 6 | --- 7 | 8 | The `markdownify` filter can render Markdown as HTML: 9 | 10 | ```html 11 | {% raw %}{{ page.title | markdownify }}{% endraw %} 12 | ``` 13 | 14 | That adds `

` tags to the output. 15 | If you're in a context where that's undesirable (for example, a heading), add two `remove` filters afterwards: 16 | 17 | ```html 18 | {% raw %}{{ page.title | markdownify | remove: '

' | remove: '

' }}{% endraw %} 19 | ``` 20 | 21 | (via [jekyll/jekyll#3571](https://github.com/jekyll/jekyll/issues/3571#issuecomment-372061718)) 22 | -------------------------------------------------------------------------------- /src/_posts/2019/2019-05-09-github-set-co-commit-credit-with-co-authored-by.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: post 3 | title: "GitHub: Set co-author commit credit with “Co-authored-by”" 4 | date: 2019-05-09 08:54:03 +0100 5 | tags: git github 6 | --- 7 | 8 | If you add the following lines to your commit message, the GitHub UI will show them as co-authors: 9 | 10 | ``` 11 | Co-authored-by: your name 12 | Co-authored-by: your co-author 13 | ``` 14 | 15 | I have a `;co` snippet for this. 16 | 17 | References: 18 | 19 | * [Indu Alagarsamy's tweet where I saw this](https://twitter.com/Indu_alagarsamy/status/1125641581904551936) 20 | * [The GitHub blog post about the feature](https://github.blog/2018-01-29-commit-together-with-co-authors/) 21 | -------------------------------------------------------------------------------- /src/_posts/2019/2019-05-12-is-apache-using-threaded-mpms-or-pre-fork.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: post 3 | title: "Is Apache using threaded MPMs or pre-fork?" 4 | date: 2019-05-12 08:56:13 +0100 5 | tags: apache dreamwidth 6 | --- 7 | 8 | As part of setting up [a Dreamwidth installation](http://wiki.dreamwidth.net/notes/Dreamwidth_Scratch_Installation#Configure_Apache_2), you have to check if Apache is using threaded MPMs or pre-fork. 9 | 10 | Even from the linked page, it wasn't clear to me how to work out which it was using. 11 | Poking around on Stack Overflow let me to this solution: 12 | 13 | ```console 14 | # apache2ctl -t -D DUMP_MODULES | grep mpm_ 15 | mpm_event_module (shared) 16 | ``` 17 | 18 | This is using threaded MPMs; you'd see a different module if it was using pre-fork. 19 | -------------------------------------------------------------------------------- /create_post.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require "fileutils" 4 | 5 | now = Time.now 6 | 7 | unless ARGV.length == 1 8 | puts "Usage: create_post.rb " 9 | exit 1 10 | end 11 | 12 | title = ARGV[0] 13 | name = title 14 | .downcase 15 | .gsub(/[^a-z]/, "-") 16 | .gsub(/\-{2,}/, "-") 17 | .chomp("-") 18 | 19 | out_dir = File.join("src", "_posts", now.strftime("%Y")) 20 | FileUtils.mkdir_p out_dir 21 | 22 | path = File.join(out_dir, "#{now.strftime('%Y-%m-%d')}-#{name}.md") 23 | 24 | def finish(path) 25 | puts path 26 | `open #{path}` 27 | exit 0 28 | end 29 | 30 | if File.exist? path 31 | finish(path) 32 | end 33 | 34 | doc = "---\nlayout: post\ntitle: \"#{title}\"\ndate: #{now}\ntags: \n---\n\n" 35 | File.open(path, 'w') { |f| f.write(doc) } 36 | 37 | finish(path) 38 | -------------------------------------------------------------------------------- /src/_posts/2018/2018-04-10-standard-ia-in-s-incurs-a-minimum-day-charge.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: post 3 | title: Standard IA in S3 incurs a minimum 30 day charge 4 | date: 2018-04-10 08:52:00 +0000 5 | tags: aws aws:s3 6 | --- 7 | 8 | From the [S3 pricing page](https://aws.amazon.com/s3/pricing/): 9 | 10 | > S3 Standard-Infrequent Access and S3 One Zone-Infrequent Access Storage are charged for a minimum storage duration of 30 days. Objects that are deleted, overwritten, or transitioned to a different storage class before 30 days will incur the normal usage charge plus a pro-rated request charge for the remainder of the 30 day minimum. 11 | 12 | This means it's less suitable for objects that are frequently replaced or deleted. 13 | 14 | (Context was the S3 bucket saving snapshots for the Catalogue API.) 15 | -------------------------------------------------------------------------------- /src/_posts/2019/2019-08-28-installing-mimetype-on-alpine-linux.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: post 3 | title: "Installing mimetype on Alpine Linux" 4 | date: 2019-08-28 13:49:55 +0100 5 | tags: alpine 6 | --- 7 | 8 | I needed to install [mimetype(1)] in Alpine because it's used by the [preview-generator] library. 9 | 10 | ```console 11 | $ docker run -it alpine sh 12 | 13 | # apk add apkbuild-cpan build-base perl perl-dev shared-mime-info 14 | # PERL_MM_USE_DEFAULT=1 cpan File::BaseDir 15 | # PERL_MM_USE_DEFAULT=1 cpan File::MimeInfo 16 | # apk del apkbuild-cpan build-base perl-dev 17 | 18 | # echo "# README" > README.md 19 | # mimetype README.md 20 | README.md: text/markdown 21 | ``` 22 | 23 | [mimetype(1)]: https://linux.die.net/man/1/mimetype 24 | [preview-generator]: https://pypi.org/project/preview-generator/ 25 | -------------------------------------------------------------------------------- /src/_includes/footer.html: -------------------------------------------------------------------------------- 1 | <footer> 2 | <div id="footer_inner"> 3 | <p> 4 | Copyright © {% include copyright_years.html %} Alex Chan. 5 | Prose is <a id="footer__cc" href="https://creativecommons.org/licenses/by/4.0/">CC-BY</a> licensed, code is <a id="footer__mit" href="https://opensource.org/licenses/MIT">MIT</a>. 6 | </p> 7 | 8 | <p id="contact_links"> 9 | Get in touch:  10 | <ul class="dot_list"> 11 | <li><a id="footer__email" href="{{ site.email | encode_mailto }}">email</a></li> 12 | <li><a id="footer__github" href="https://github.com/{{ site.social.github }}">github</a></li> 13 | <li><a id="footer__twitter" href="https://twitter.com/{{ site.social.twitter }}">twitter</a></li> 14 | </ul> 15 | </p> 16 | </div> 17 | </footer> 18 | -------------------------------------------------------------------------------- /src/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: page 3 | --- 4 | 5 | ## Notebook entries 6 | 7 | <div id="notebook_filters"> 8 | Filtering to notes tagged with 9 | </div> 10 | 11 | <ul id="notebook_index"> 12 | {% for post in site.posts %} 13 | <li class="{% for tag in post.tags %}tagged_with_{{ tag }} {% endfor %}"> 14 | <div> 15 | <div class="notebook_index__url"> 16 | <a href="{{ post.url }}">{{ post.title | smartify }}</a> <br/> 17 | </div> 18 | {% if post.tags %} 19 | <div class="notebook_index__tags"> 20 | Tagged with: 21 | {% assign sorted_tags = post.tags | sort %} 22 | {% for tag in sorted_tags %} 23 | <a href="#" onclick="filterToTag('{{ tag }}')">{{ tag }}</a> 24 | {% endfor %} 25 | </div> 26 | {% endif %} 27 | </div> 28 | </li> 29 | {% endfor %} 30 | </ul> 31 | -------------------------------------------------------------------------------- /src/_posts/2018/2018-03-05-sharing-files-with-my-work-computer-using-dropbox.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: post 3 | title: Sharing files with my work computer using Dropbox 4 | date: 2018-03-05 20:02:00 +0000 5 | tags: dropbox 6 | --- 7 | 8 | Problem: I have some files in my Dropbox that I want to search with my work computer (notes, my Alfred preferences, maybe a few other things). 9 | 10 | I could log into my personal Dropbox at work, and then use Selective Sync to download just those folders locally, but that feels like it's skirting around poor data management practices. 11 | 12 | Solution: I created a new Dropbox account with my work email, then shared the directories I want at work into that account. 13 | I get granular control of what's shared with work, and it's harder for stuff to accidentally get into my personal account. 14 | -------------------------------------------------------------------------------- /src/_posts/2019/2019-04-20-getting-the-cover-of-an-epub-file.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: post 3 | title: "Getting the cover of an epub file" 4 | date: 2019-04-20 21:24:30 5 | tags: epub 6 | --- 7 | 8 | The preview-generator library doesn't have support for epub files, so I have to create thumbnails for those separately. 9 | The first step is to get the cover image of the book. 10 | 11 | I found some useful code for doing this on GitHub: <https://github.com/marianosimone/epub-thumbnailer> (GPL) 12 | 13 | The basic gist: 14 | 15 | * An epub is a zip file, so look inside the zipfile and assume the biggest image entry is the cover image 16 | * Poke around inside the `container.xml` inside the epub 17 | 18 | The code in the GitHub repo is Python, but is fairly simple and could be ported to another language if necessary, licence allowing. 19 | -------------------------------------------------------------------------------- /src/_posts/2018/2018-11-26-how-to-suppress-installing-rdoc-ri-docs-when-running-gem-install.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: post 3 | title: How to suppress installing rdoc/ri docs when running 'gem install' 4 | date: 2018-11-26 10:13:25 +0000 5 | tags: ruby docker 6 | --- 7 | 8 | If you try to run `gem install` in a Docker container which only contains a Ruby package (and no rdoc or ri), you get an error: 9 | 10 | ``` 11 | Step 3/5 : RUN gem install rack 12 | ---> Running in 8ee24453f7a9 13 | ERROR: While executing gem ... (Gem::DocumentError) 14 | RDoc is not installed: cannot load such file -- rdoc/rdoc 15 | ``` 16 | 17 | If you add the following line to your `.gemrc`, it skips trying to install the docs. 18 | 19 | ``` 20 | install: --no-rdoc --no-ri 21 | ``` 22 | 23 | Handy if you're in a Docker image that will never run interactively! 24 | -------------------------------------------------------------------------------- /src/_posts/2018/2018-10-24-using-xargs-for-parallel-processing.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: post 3 | title: Using xargs for parallel processing 4 | date: 2018-10-24 08:55:00 +0000 5 | tags: shell 6 | --- 7 | 8 | If you have a file full of arguments (`inputs.txt`), and a script that takes a single ID as an argument (`process_single_id.py`), you can run the script in parallel with `xargs`: 9 | 10 | ```shell 11 | $ xargs -P 84 -I '{}' python process_single_id.py '{}' < inputs.txt 12 | ``` 13 | 14 | Customise the number of parallel processes with the `-P` flag. 15 | 16 | You'll want to experiment with the number of processes you run -- although 84 was about the limit of my laptop's CPU, it caused more errors in the Tandem Vault/S3 APIs, so the overall throughput was actually less. 17 | 18 | (I discovered this during the Miro migration project in October 2018.) 19 | 20 | -------------------------------------------------------------------------------- /src/_posts/2019/2019-05-11-how-do-dreamwidth-post-ids-increment.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: post 3 | title: "Dreamwidth: How/why do post IDs increment?" 4 | date: 2019-05-11 23:26:38 +0100 5 | tags: dreamwidth 6 | --- 7 | 8 | Dreamwidth IDs (posts, comments) don't increment one-by-one, but via algebraic manipulations. Comments don't go 1, 2, 3, they go 256, 540, 721, whatever. 9 | 10 | The exact pattern is [something like](https://github.com/dreamwidth/dw-free/blob/2c5f1a9a11efbcf43a9eaa73a6ae43a533ec439d/cgi-bin/DW/Collection.pm#L54): 11 | 12 | ``` 13 | display_id = collection_id * 256 + internal_id 14 | ``` 15 | 16 | This is an old anti-bot measure: if a bot saw URLs with `/1.html`, `/2.html`, `/3.html`, it assumes it's a sequence and continues in progression, which could overwhelm the site. 17 | These days it's not needed (hooray CDNs!) but it's baked into the site now. 18 | -------------------------------------------------------------------------------- /src/_scss/_aside.scss: -------------------------------------------------------------------------------- 1 | aside { 2 | border-bottom: 2px solid $primary-color; 3 | background: url('/theme/specktre.png') rgba(114,83,237,0.25); 4 | background-size: auto 100%; 5 | 6 | #aside_inner { 7 | @include central_element(); 8 | 9 | font-family: Georgia, Palatino, 'Palatino Linotype', Times, 'Times New Roman', serif;; 10 | 11 | a:not([class]) { 12 | background-image: none; 13 | } 14 | 15 | a:hover { 16 | text-decoration: none; 17 | } 18 | 19 | a:visited { 20 | color: white; 21 | } 22 | 23 | #brand { 24 | margin-bottom: 18px; 25 | font-weight: normal; 26 | font-size: 2em; 27 | a { 28 | text-decoration: none; 29 | } 30 | } 31 | 32 | #aside_links ul { 33 | padding-top: 0px; 34 | padding-bottom: 0px; 35 | margin-bottom: 10px; 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/_scss/_settings.scss: -------------------------------------------------------------------------------- 1 | $primary-color: darken(#7253ed, 10%); 2 | $primary-dark: darken($primary-color, 25%); 3 | 4 | $main-font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; 5 | $mono-font: Menlo, Consolas, monospace; 6 | 7 | $body-color: #3c3942; 8 | $accent-grey: #999; 9 | $light-grey: #f0f0f0; 10 | $midtone-gray: #ccc; 11 | 12 | $default-font-size: 1em; 13 | $line-height: 1.45em; 14 | 15 | $meta-size: 0.82; 16 | $meta-font-size: $meta-size * $default-font-size; 17 | $meta-line-height: $meta-size * $line-height * 1.15; 18 | 19 | $max-width: 750px !default; 20 | $default-padding: 20px !default; 21 | $big-font-size: 2em !default; 22 | 23 | $sidebar-border-width: 1px; 24 | 25 | $scaling-factor: 0.95; 26 | $code-scaling-factor: 0.88; 27 | 28 | $jekyll-red: #d50000; 29 | $linode-green: green; 30 | $github-grey: #171515; 31 | $twitter-blue: #55acee; 32 | -------------------------------------------------------------------------------- /src/_posts/2019/2019-04-20-http-the-content-disposition-header.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: post 3 | title: "HTTP: The Content-Disposition header" 4 | date: 2019-04-20 21:24:30 +0100 5 | tags: web-dev http 6 | --- 7 | 8 | The `Content-Disposition` header can be used to tell a browser the filename of an HTTP response. 9 | It's used for "Save As" or when downloading the file. 10 | 11 | For example, you might access a URL of the form: 12 | 13 | /files/0645c33f-0be6-44e1-8059-228ec9594867.pdf 14 | 15 | If you include a `Content-Disposition` header of the form: 16 | 17 | filename*=utf-8''original_filename.pdf 18 | 19 | the browser will download this filename as `original_filename.pdf`. 20 | 21 | Read more: 22 | 23 | * MDN docs for Content-Disposition: <https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Disposition> 24 | * Encoding a filename as UTF-8: <https://stackoverflow.com/a/49481671/1558022> 25 | -------------------------------------------------------------------------------- /src/_posts/2018/2018-11-05-be-careful-of-assembling-partial-databases-with-concurrency.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: post 3 | title: Be careful of assembling partial databases with concurrency 4 | date: 2018-11-05 16:30:20 +0000 5 | tags: concurrency miro-migration 6 | --- 7 | 8 | This bit me during the Miro migration. 9 | 10 | A brief reminder of the setup: I had an "inventory" of destinations for each of the images. 11 | When I ran a script to move an image, it wrote a "partial inventory" update, which could be reassembled into the proper inventory later. 12 | In practice, these are all JSON files on disk. 13 | 14 | ![](/images/partial_inventory.png) 15 | 16 | Remembering to run the reassembly script was tedious, so I had the "smart" idea of writing the partial file and then automatically triggering the inventory script. 17 | 18 | But because the main JSON file can't be updated concurrently, this promptly exploded. 19 | Don't do that! 20 | -------------------------------------------------------------------------------- /src/_posts/2018/2018-10-24-using-force-with-lease.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: post 3 | title: Using -​-force-with-lease 4 | date: 2018-10-24 09:20:00 +0000 5 | tags: git 6 | --- 7 | 8 | When doing a `git push`, if there are different commits in the remote, your push is rejected. 9 | This often happens if I've rebased against master. 10 | 11 | You can get round this by running `git push --force`, but what if your rebase wasn't the only change? 12 | What if somebody else pushed commits while you weren't looking? 13 | This can be dangerous! 14 | 15 | Using `git push --force-with-lease` is safer, because it checks the branch hasn't moved in the meantime. 16 | Quoting [`--force` considered harmful; understanding git's `--force-with-lease`](https://developer.atlassian.com/blog/2015/04/force-with-lease/): 17 | 18 | > What -​-force-with-lease does is refuse to update a branch unless it is the state that we expect; i.e. nobody has updated the branch upstream. 19 | -------------------------------------------------------------------------------- /src/_posts/2018/2018-10-28-replacing-map-flatten-with-flatmap.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: post 3 | title: Replacing .map.flatten with .flatMap 4 | date: 2018-10-28 09:30:00 +0000 5 | tags: scala 6 | --- 7 | 8 | This is something IntelliJ whinges about. 9 | I'm still not entirely sure what `flatMap` does, but here's an example which illustrates the change I made. 10 | 11 | Before: 12 | 13 | ```scala 14 | val listOfListOfNames: List[List[String]] 15 | 16 | val people: List[People] = 17 | listOfListOfNames 18 | .map { names: List[String] => 19 | val maybeFirst: Option[String] = getFirstName(names) 20 | maybeFirst.map { name => Person(name) } 21 | } 22 | .flatten 23 | ``` 24 | 25 | After: 26 | 27 | ```scala 28 | val people: List[People] = 29 | listOfListOfNames 30 | .flatMap { names: List[String] => 31 | val maybeFirst: Option[String] = getFirstName(names) 32 | maybeFirst.map { name => Person(name) } 33 | } 34 | ``` 35 | -------------------------------------------------------------------------------- /src/_posts/2018/2018-11-12-failing-to-find-an-implicit-objectstore-when-it-wants-an-executioncontext.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: post 3 | title: Failing to find an implicit ObjectStore when it wants an ExecutionContext 4 | date: 2018-11-12 15:49:07 +0000 5 | tags: scala wellcome 6 | --- 7 | 8 | I was getting a complaint about being unable to find an implicit ObjectStore when running some Goobi reader tests: 9 | 10 | ``` 11 | GoobiReaderFeatureTest.scala:75: could not find implicit value for parameter objectStore: uk.ac.wellcome.storage.ObjectStore[java.io.InputStream] 12 | 13 | withTypeVHS[InputStream, GoobiRecordMetadata, R](bucket, table) { vhs => 14 | ^ 15 | ``` 16 | 17 | I have no idea why that was the error message, but to save future head-scratching, this was the import I needed to add before it compiled: 18 | 19 | ```scala 20 | import scala.concurrent.ExecutionContext.Implicits.global 21 | ``` 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # notebook.alexwlchan.net 2 | 3 | This repo has the code for my notebook site, [notebook.alexwlchan.net][root]. 4 | 5 | My notebook is for short blog posts that I don't think are worth writing up as a full post on my [main site][main], but which are useful information that I want to find later. 6 | I make them public so they can be indexed by Google. 7 | Often it's for solutions to very specific problems. 8 | 9 | Not everything will make sense if you're not me -- these are written for me first, and may be missing context or assumptions that I've forgotten I'm making. 10 | On my main blog I try to write those out explicitly, but here I don't bother. 11 | 12 | ![](screenshot.png) 13 | 14 | This is a static site built with [Jekyll][jekyll], using a bunch of build machinery taken from my main site. 15 | 16 | [root]: https://notebook.alexwlchan.net/ 17 | [main]: https://github.com/alexwlchan/alexwlchan.net 18 | [jekyll]: https://jekyllrb.com/ 19 | 20 | # License 21 | 22 | MIT. 23 | -------------------------------------------------------------------------------- /src/_posts/2019/2019-05-11-hashes-and-hash-references.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: post 3 | title: "Perl: Hashes and hash references" 4 | date: 2019-05-11 21:37:24 +0100 5 | tags: perl 6 | --- 7 | 8 | Working in [the Dreamwidth codebase][s2_pm]: 9 | 10 | ```perl 11 | sub sitescheme_secs_to_iso { 12 | my ( $secs, %opts ) = @_; 13 | 14 | ... 15 | 16 | # if opts has a true tz key, get the remote user's timezone if possible 17 | if ( $opts{tz} ) { 18 | ``` 19 | 20 | It wasn't clear to me what the difference is between `%opts` and `$opts` is. 21 | 22 | I asked in Discord and [momijizukamori] explained that swapping `%opts` to `$opts` changes it from a hash to a hash reference. 23 | You can only access the values of a hash reference as `$hash{key}`, and changing it to `$hash->{key}` should fix it. 24 | 25 | [s2_pm]: https://github.com/dreamwidth/dw-free/blob/fa394ce0e47ea83d5b5d0db994de324b049a9ccb/cgi-bin/LJ/S2.pm#L2640-L2656 26 | [momijizukamori]: https://momijizukamori.dreamwidth.org/ 27 | -------------------------------------------------------------------------------- /src/_posts/2018/2018-11-28-beware-of-dynamic-arguments-in-apply-methods.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: post 3 | title: "Scala: Beware of dynamic arguments in apply() methods" 4 | date: 2018-11-28 13:41:00 +0000 5 | tags: scala 6 | --- 7 | 8 | Here's a minimal of a case class we had in the platform: 9 | 10 | ```scala 11 | import java.time.Instant 12 | 13 | case class Modifiable( 14 | createdDate: Instant = Instant.now, 15 | lastModifiedDate: Instant = Instant.now 16 | ) 17 | ``` 18 | 19 | We had a test that created an instance of `Modifiable`, then asserted that the creation 20 | and last modified date were the same -- and normally that's fine. 21 | 22 | But occasionally there'd be a delay, and you'd get a new instance of `Modifiable` that had a different created and last modified date. 23 | The fix: 24 | 25 | ```scala 26 | case object Modifiable { 27 | def apply(createdDate: Instant = Instant.now): Modifiable = 28 | Modifiable( 29 | createdDate = createdDate, 30 | lastModifiedDate = createdDate 31 | ) 32 | } 33 | ``` 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2018 Alex Chan 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /src/_posts/2018/2018-11-15-notes-on-vpc-networking-and-acls.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: post 3 | title: Notes on AWS networking and ACLs 4 | date: 2018-11-15 13:05:46 +0000 5 | tags: aws networking 6 | --- 7 | 8 | Some brief notes from a whiteboard session with RK. 9 | 10 | ![](/images/vpc_networking.png) 11 | 12 | An availability zone is assigned a CIDR block (here `120.0.0.0/16`). 13 | 14 | The AZ has a default route table and ACL (access control list), which let everything through. 15 | 16 | Within the AZ, you create subnets. 17 | There are ACLs attached to the subnets, and route table entries on the subnets. 18 | (This lets you keep public and private subnets separate, and have different routes to the public Internet.) 19 | 20 | The route table has the VPC CIDR, and the Internet gateway (or a NAT Gateway for private subnets). 21 | 22 | ACL rules take precedence over security rules. 23 | They have to be stateless -- only for inbound traffic or only for outbound traffic. 24 | 25 | **Don't play with ACL and route table entries! That way lies unplanned outages.** 26 | -------------------------------------------------------------------------------- /src/_posts/2019/2019-08-14-condition-parameter-type-does-not-match-schema-type.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: post 3 | title: "Condition parameter type does not match schema type" 4 | date: 2019-08-14 16:48:07 +0100 5 | tags: aws aws:dynamodb 6 | --- 7 | 8 | If you get the following error from DynamoDB: 9 | 10 | > One or more parameter values were invalid: Condition parameter type does not match schema type 11 | 12 | it's a sign that you're requesting the wrong type of paramater in a query. 13 | 14 | In boto3/Python, it's code like: 15 | 16 | ```python 17 | import boto3 18 | 19 | dynamodb = boto3.client("dynamodb") 20 | 21 | dynamodb.query( 22 | TableName="storage-staging-ingests", 23 | KeyConditions={ 24 | "id": { 25 | "AttributeValueList": [{"N": "1"}], 26 | "ComparisonOperator": "EQ" 27 | } 28 | } 29 | ) 30 | ``` 31 | 32 | In Scala-land, we've seen this when we pass a case class as the key (e.g. `ReplicaPath`) and the custom DynamoFormat is wrong or missing, so it tries to query on a map instead of a string/int. 33 | -------------------------------------------------------------------------------- /src/_posts/2018/2018-10-24-beware-ambiguous-dates-with-dateutil-parse.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: post 3 | title: Beware ambiguous dates with dateutil.parse 4 | date: 2018-10-24 09:01:00 +0000 5 | tags: python datetime-handling 6 | --- 7 | 8 | The [dateutil module](https://pypi.org/project/python-dateutil/) is useful for parsing ambiguous dates, but you want to be careful parsing lists -- it defaults to American "month-first" style dates. 9 | Compare: 10 | 11 | ```python 12 | import dateutil.parser as dp 13 | 14 | dates = ["1/2/2018", "11/2/2018", "21/2/2018"] 15 | 16 | for date_str in dates: 17 | print(dp.parse(date_str)) 18 | 19 | # 2018-01-02 00:00:00 20 | # 2018-11-02 00:00:00 21 | # 2018-02-21 00:00:00 22 | 23 | for date_str in dates: 24 | print(dp.parse(date_str, dayfirst=True)) 25 | 26 | # 2018-02-01 00:00:00 27 | # 2018-02-11 00:00:00 28 | # 2018-02-21 00:00:00 29 | ``` 30 | 31 | If you don't pass the `dayfirst=True`, it makes a guess at what seems sensible, and only uses the British format if it's unambiguous. 32 | This can cause unexpected results! 33 | -------------------------------------------------------------------------------- /src/_posts/2019/2019-05-15-getting-the-latest-version-of-a-range-key.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: post 3 | title: "DynamoDB: Getting the latest version of a range key associated with a hash key" 4 | date: 2019-05-15 20:25:24 +0100 5 | tags: aws aws:dynamodb java scala 6 | --- 7 | 8 | I have yet to come up with a way to describe this that isn't completely horrible; this DynamoDB query with the document client will get you the lowest/highest range key row associated with a particular hash key value 9 | 10 | ```scala 11 | val querySpec = new QuerySpec() 12 | .withHashKey(hashKeyName, hashKeyValue) 13 | .withConsistentRead(true) 14 | .withScanIndexForward(lowestValueFirst) 15 | .withMaxResultSize(1) 16 | ``` 17 | 18 | A query returns results ordered by range key -- the trick is making sure results arrive in the right order. 19 | 20 | See also: 21 | 22 | * [DynamoHashKeyLookup.scala @ cc3b434](https://github.com/wellcometrust/scala-storage/blob/cc3b434c5cfeb264f14e7da0504dbf796a528141/storage/src/main/scala/uk/ac/wellcome/storage/dynamo/DynamoHashKeyLookup.scala) (MIT) 23 | -------------------------------------------------------------------------------- /src/_posts/2018/2018-11-06-beware-of-using-val-in-abstract-traits.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: post 3 | title: Beware of NullPointerException when using 'val' in abstract traits 4 | date: 2018-11-06 13:37:57 +0000 5 | tags: scala 6 | --- 7 | 8 | A simplified version of the code I was using is below: 9 | 10 | ```scala 11 | class NotifierApp() extends WellcomeApp { 12 | val configModule = new Configurable { } 13 | } 14 | 15 | trait WellcomeApp { 16 | val configModule: Configurable 17 | val injector: Injector = Guice.createInjector(configModule) 18 | } 19 | ``` 20 | 21 | Whenever I tried to run it, the code threw a NullPointerException while creating the Guice injector. 22 | Why? 23 | 24 | I think it's because `injector` was being evaluated when the trait was created, and before the new value of `configModule` had been set in `NotifierApp`. 25 | Thus `injector` is nil, and that blows up the injector. 26 | Changing this to use "def" instead of "val" seemed to fix the bug. 27 | 28 | For more context, see [wellcometrust/platform#2971](https://github.com/wellcometrust/platform/pull/2971). 29 | -------------------------------------------------------------------------------- /src/_plugins/cleanup_text.rb: -------------------------------------------------------------------------------- 1 | # Various text cleanups. 2 | 3 | module Jekyll 4 | module CleanupsFilter 5 | def cleanup_text(input) 6 | # Replace mentions of RFCs with a non-breaking space version. 7 | text = input.gsub(/RFC (\d+)/, 'RFC \1') 8 | 9 | # Also: "part X" or "Part X" 10 | text = text.gsub(/([Pp]art) (\d+)/, '\1 \2') 11 | 12 | # Display "LaTeX" in a nice way, if you have CSS enabled 13 | text = text.gsub( 14 | "LaTeX", 15 | "<span class=\"latex\">L<sup>a</sup>T<sub>e</sub>X</span>") 16 | 17 | # Replace any mention of "PyCon" with the appropriate non-breaking space 18 | text = text.gsub("PyCon ", "PyCon ") 19 | 20 | # Get rid of the trailing space after the dollar in language-console 21 | # blocks. The space is added in CSS and is unselectable. 22 | text = text.gsub( 23 | "<span class=\"w\">$ </span>", 24 | "<span class=\"w\">$</span>") 25 | 26 | text 27 | end 28 | end 29 | end 30 | 31 | Liquid::Template::register_filter(Jekyll::CleanupsFilter) 32 | -------------------------------------------------------------------------------- /src/_posts/2019/2019-04-20-javascript-manipulating-url-query-parameters.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: post 3 | title: "JavaScript: manipulating URL query parameters" 4 | date: 2019-04-20 21:24:30 +0100 5 | tags: javascript 6 | --- 7 | 8 | Last time I did this, I had to use some moderately fiddly code from Stack Overflow. 9 | There are built-in tools for this now: 10 | 11 | ```javascript 12 | function addQueryParameter(name, value) { 13 | var url = new URL(window.location.href); 14 | url.searchParams.append(name, value); 15 | return url.href 16 | } 17 | 18 | function setQueryParameter(name, value) { 19 | var url = new URL(window.location.href); 20 | url.searchParams.set(name, value); 21 | return url.href 22 | } 23 | 24 | function deleteQueryParameter(name) { 25 | var url = new URL(window.location.href); 26 | url.searchParams.delete(name); 27 | return url.href 28 | } 29 | ``` 30 | 31 | All these methods return a string based on the current window location. 32 | 33 | Read more: 34 | 35 | * MDN docs for URLSearchParams: <https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams> 36 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | BUILD_IMAGE = jekyll/jekyll:3.8 2 | 3 | RSYNC_HOST = 139.162.244.147 4 | RSYNC_USER = alexwlchan 5 | RSYNC_DIR = /home/alexwlchan/sites/notebook.alexwlchan.net 6 | 7 | ROOT = $(shell git rev-parse --show-toplevel) 8 | DST = $(ROOT)/_site 9 | 10 | 11 | build: 12 | docker run --tty --rm \ 13 | --volume $(ROOT):$(ROOT) \ 14 | --workdir $(ROOT) \ 15 | --env JEKYLL_UID=0 \ 16 | $(BUILD_IMAGE) jekyll build 17 | 18 | serve: 19 | docker run \ 20 | --publish 6060:6060 \ 21 | --volume $(ROOT):$(ROOT) \ 22 | --workdir $(ROOT) \ 23 | --tty --rm $(BUILD_IMAGE) \ 24 | jekyll serve --host "0.0.0.0" --port 6060 --watch --drafts 25 | 26 | deploy: build 27 | docker run --rm --tty \ 28 | --volume ~/.ssh/id_rsa:/root/.ssh/id_rsa \ 29 | --volume $(DST):/data \ 30 | instrumentisto/rsync-ssh \ 31 | rsync \ 32 | --archive \ 33 | --verbose \ 34 | --compress \ 35 | --delete \ 36 | --exclude=".DS_Store" \ 37 | --rsh="ssh -o StrictHostKeyChecking=no -i ~/.ssh/id_rsa" \ 38 | /data/ "$(RSYNC_USER)"@"$(RSYNC_HOST)":"$(RSYNC_DIR)" 39 | 40 | 41 | .PHONY: build serve deploy 42 | -------------------------------------------------------------------------------- /src/_posts/2019/2019-05-09-jekyll-creating-permalinks-to-posts.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: post 3 | title: "Jekyll: creating permalinks to posts" 4 | date: 2019-05-09 12:39:53 +0100 5 | tags: jekyll 6 | --- 7 | 8 | It's a bit buried in the Jekyll docs, but you can use the `post_url` tag to generate permalink URLs to posts: 9 | 10 | ``` 11 | {% raw %}{% post_url 2019-05-09-jekyll-creating-permalinks-to-posts %}{% endraw %} 12 | ``` 13 | 14 | If you get this warning: 15 | 16 | > Deprecation: A call to {% raw %}{% post_url 2019-05-09-java-conditional-updates-in-dynamodb %}{% endraw %} did not match a post using the new matching method of checking name (path-date-slug) equality. Please make sure that you change this tag to match the post's name exactly. 17 | 18 | It's because I put each year of posts in a separate folder. 19 | You need to prefix the year to get the "path" part, like so: 20 | 21 | ``` 22 | {% raw %}{% post_url 2019/2019-05-09-jekyll-creating-permalinks-to-posts %}{% endraw %} 23 | ``` 24 | 25 | References: 26 | 27 | - [Tags Filters/Linking to posts](https://jekyllrb.com/docs/liquid/tags/#linking-to-posts) in the Jekyll docs 28 | -------------------------------------------------------------------------------- /src/_posts/2019/2019-04-20-python-include-the-filename-content-type-and-content-length-in-a-requests-upload.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: post 3 | title: "Python: Include the filename, Content-Type and Content-Length in a requests upload" 4 | date: 2019-04-20 21:24:30 +0100 5 | tags: python 6 | --- 7 | 8 | When you upload a file through an HTML form: 9 | 10 | ```html 11 | <form action="/upload" method="post" enctype="multipart/form-data"> 12 | <input name="file" type="file"> 13 | ... 14 | </form> 15 | ``` 16 | 17 | it gets sent to the server with a filename, content-type and content-length (along with the contents, of course). 18 | 19 | You can pass optional content-type and filename when uploading a file with requests by passing a 2-tuple or 3-tuple in the `files` list. 20 | Two examples: 21 | 22 | ```python 23 | import requests 24 | 25 | requests.post( 26 | "/upload", 27 | files={"file": ("mydocument.pdf", open("mydocument.pdf", "rb"))}, 28 | ) 29 | 30 | requests.post( 31 | "/upload", 32 | files={"file": ("mydocument.pdf", open("mydocument.pdf", "rb"), "application/pdf")}, 33 | ) 34 | ``` 35 | 36 | Relevant docs: <https://2.python-requests.org/en/master/api/#requests.request> 37 | -------------------------------------------------------------------------------- /src/_posts/2019/2019-04-20-python-use-the-whitenoise-library-to-serve-static-files.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: post 3 | title: "Python: Use the whitenoise library to serve static files" 4 | date: 2019-04-20 21:24:30 +0100 5 | tags: python 6 | --- 7 | 8 | [whitenoise] is a Python library for serving static files in a WSGI application. 9 | When you create an instance of whitenoise, you pass it a folder, and it learns all the files in that folder. 10 | If you save a new file in that folder, you need to tell whitenoise about it. 11 | 12 | Here's a quick example, taken from the [whitenoise docs][wn_docs]: 13 | 14 | ```python 15 | from whitenoise import WhiteNoise 16 | 17 | from my_project import MyWSGIApp 18 | 19 | application = MyWSGIApp() 20 | application = WhiteNoise(application, root='/path/to/static/files') 21 | application.add_files('/path/to/more/static/files', prefix='more-files/') 22 | ``` 23 | 24 | It gets unhappy if the size of a file changes underneath it, after the initial load. 25 | 26 | Read more: 27 | 28 | * Whitenoise docs: <http://whitenoise.evans.io/en/stable/> 29 | 30 | [whitenoise]: https://pypi.org/project/whitenoise/ 31 | [wn_docs]: http://whitenoise.evans.io/en/stable/#quickstart-for-other-wsgi-apps 32 | -------------------------------------------------------------------------------- /src/_posts/2018/2018-11-07-logging-into-the-aws-console-when-your-alias-isn-t-working.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: post 3 | title: Logging into the AWS Console when your alias isn't working 4 | date: 2018-11-07 10:47:58 +0000 5 | tags: aws 6 | --- 7 | 8 | Trying to log into the AWS Console today, and we all saw this error: 9 | 10 | <img src="/images/cant-login-to-aws.png" style="width: 402px;"> 11 | 12 | Meep! 13 | Had our account been compromised? 14 | (Spoiler: no.) 15 | 16 | Our IAM accounts were all working through the CLI, and we eventually tracked down the root credentials -- and then discovered that the IAM account alias had changed. 17 | No longer was the account alias *wellcomedigitalplatform*, instead it was *otherthing*. 18 | 19 | Two ways we could have worked around this: 20 | 21 | 1. Logging in with the account ID (7600...) instead of the account alias. 22 | This always works, whether or not you have an alias set. 23 | 24 | 2. Used the AWS CLI to discover the current aliases (if any): 25 | 26 | ```console 27 | $ aws iam list-account-aliases 28 | { 29 | "AccountAliases": [ 30 | "wellcomedigitalplatform" 31 | ] 32 | } 33 | 34 | $ aws iam list-account-aliases 35 | { 36 | "AccountAliases": [] 37 | } 38 | ``` -------------------------------------------------------------------------------- /src/_plugins/escape_email.rb: -------------------------------------------------------------------------------- 1 | # This does some quick HTML encoding on email addresses to make them 2 | # slightly harder to find for spam bots. The idea and implementation 3 | # are both copied directly from Markdown.pl. 4 | 5 | require 'cgi' 6 | 7 | module Jekyll 8 | module EmailFilter 9 | def encode_mailto(input) 10 | "mailto:#{input}".chars.map { |ch| _encode_char(ch) }.join("") 11 | end 12 | 13 | def encode_mail(input) 14 | "#{input}".chars.map { |ch| _encode_char(ch) }.join("") 15 | end 16 | 17 | def _encode_char(char) 18 | if char == ":" 19 | char 20 | elsif char == "@" 21 | _encode_char_with_method(char, method = "hex") 22 | else 23 | r = rand() 24 | if r > 0.9 25 | _encode_char_with_method(char) 26 | elsif r < 0.45 27 | _encode_char_with_method(char, method = "hex") 28 | else 29 | _encode_char_with_method(char, method = "dec") 30 | end 31 | end 32 | end 33 | 34 | def _encode_char_with_method(char, method = nil) 35 | if method == "hex" 36 | "&#x#{char.ord.to_s(16).upcase};" 37 | elsif method == "dec" 38 | "&##{char.ord};" 39 | else 40 | char 41 | end 42 | end 43 | 44 | end 45 | end 46 | 47 | Liquid::Template::register_filter(Jekyll::EmailFilter) 48 | -------------------------------------------------------------------------------- /src/_scss/_footer.scss: -------------------------------------------------------------------------------- 1 | footer { 2 | border-top: 1px solid $light-grey; 3 | padding-top: 15px; 4 | padding-bottom: 15px; 5 | 6 | @media print { 7 | display: none; 8 | } 9 | } 10 | 11 | #footer_inner { 12 | @include central_element(); 13 | 14 | font-size: $meta-font-size; 15 | &, a, a:visited { 16 | color: $accent-grey; 17 | } 18 | 19 | #contact_links, ul.dot_list { 20 | display: inline-block; 21 | margin-top: 0px; 22 | } 23 | 24 | p:first-child { 25 | margin-bottom: 0px; 26 | } 27 | 28 | // These definitions have to live in the #footer_inner block so they 29 | // override the a:visited style defined above. 30 | #footer__cc:hover, #footer__mit:hover { 31 | color: black; 32 | background-image: linear-gradient(rgba(0, 0, 0, 0.45) 0%, rgba(0, 0, 0, 0.45) 100%); 33 | background-size: 1px 1px 34 | } 35 | 36 | #footer__email:hover, #footer__about:hover { 37 | color: $primary-color; 38 | } 39 | 40 | #footer__github:hover { 41 | color: $github-grey; 42 | background-image: linear-gradient(rgba(23, 21, 21, 0.45) 0%, rgba(23, 21, 21, 0.45) 100%); 43 | background-size: 1px 1px 44 | } 45 | #footer__twitter:hover { 46 | color: $twitter-blue; 47 | background-image: linear-gradient(rgba(85, 172, 238, 0.45) 0%, rgba(85, 172, 238, 0.45) 100%); 48 | background-size: 1px 1px 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/_scss/_mixins.scss: -------------------------------------------------------------------------------- 1 | @mixin centred($max-width) { 2 | max-width: $max-width; 3 | margin-left: auto; 4 | margin-right: auto; 5 | } 6 | 7 | @mixin central_element() { 8 | @include centred($max-width); 9 | padding: 1px $default-padding; 10 | } 11 | 12 | @mixin disable_select() { 13 | // Disable text selection highlighting 14 | // https://stackoverflow.com/a/4407335/1558022 15 | -webkit-touch-callout: none; /* iOS Safari */ 16 | -webkit-user-select: none; /* Safari */ 17 | -khtml-user-select: none; /* Konqueror HTML */ 18 | -moz-user-select: none; /* Firefox */ 19 | -ms-user-select: none; /* Internet Explorer/Edge */ 20 | user-select: none; /* Non-prefixed version, currently 21 | supported by Chrome and Opera */ 22 | } 23 | 24 | @mixin purple_box() { 25 | background-color: rgba(114, 83, 237, 0.04); 26 | border: $sidebar-border-width solid rgba(114, 83, 237, 0.45); 27 | border-radius: 5px; 28 | } 29 | 30 | @mixin fullwidth_box() { 31 | padding: ($default-padding / 2) ($default-padding * 0.6 - $sidebar-border-width); 32 | line-height: $line-height * $code-scaling-factor * 1.08; 33 | 34 | margin-left: -$default-padding * 0.6; 35 | margin-right: -$default-padding * 0.6; 36 | 37 | @media screen and (max-width: $max-width + $default-padding * 3) { 38 | margin-left: 0px; 39 | } 40 | } -------------------------------------------------------------------------------- /src/_posts/2018/2018-11-06-running-after-deleting-the-overlay-directory.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: post 3 | title: Running Docker after deleting the /var/lib/docker/overlay2 directory 4 | date: 2018-11-06 08:07:17 +0000 5 | tags: docker 6 | --- 7 | 8 | When my Linode wouldn't boot, I managed to get in via rescue mode, and I thought maybe the boot disk was full -- so I went looking for big files to delete. 9 | More than half the disk was taken up by `/var/lib/docker/overlay2`. 10 | The VM was already hosed, so trashing Docker wouldn't make it worse! 11 | Thus: 12 | 13 | ```console 14 | $ rm -rf /var/lib/docker/overlay2 15 | $ mkdir -p /var/lib/docker/overlay2 16 | ``` 17 | 18 | Thanks to Linode support, I got the box up and running, but now trying to run any Docker commands fails with errors like: 19 | 20 | ``` 21 | No such file or directory: /var/lib/docker/overlay2/a37c8253bbefa7ea641a110a5e6e2f5efd7d403f89b3319ef97a8038c2db229b 22 | ``` 23 | 24 | All the image/container definitions live in this directory, but are indexed separately -- so Docker still thought it had a complete collection of images and containers. 25 | When I asked it to run an image, it failed because it couldn't find the local image it thought it had. 26 | 27 | Fix was to purge the index of local images and containers: 28 | 29 | ```console 30 | $ docker rm $(docker ps -a -q) 31 | $ docker rmi $(docker images -q) 32 | ``` 33 | -------------------------------------------------------------------------------- /src/_posts/2018/2018-09-17-list-all-git-object-ids-and-their-type.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: post 3 | title: List all Git object IDs and their type 4 | date: 2018-09-17 12:39:00 +0000 5 | tags: git 6 | --- 7 | 8 | From Michal Grochmal in the PyCon UK Slack: 9 | 10 | ```console 11 | $ find .git/objects/ -type f | sed -e s^\.git/objects/^^ -e s^/^^ | sort | while read x; do echo -n "$x "; git cat-file -t $x; done 12 | ``` 13 | 14 | That didn't work for me on macOS, because I have a different version of sed (I think). 15 | This is less pretty but has the same effect. 16 | 17 | ```console 18 | $ find .git/objects -type f | tr '/' ' ' | awk '{print $3 $4}' | grep -v pack | while read x; do echo -n "$x "; git cat-file -t "$x"; done 19 | ``` 20 | 21 | With either command, output is of the form: 22 | 23 | ``` 24 | 12779b2e3b24fded5f817525a416a625e9f1a356 tree 25 | 38246dfb9d83a2c7be5ee0dda3a62cb223e9a764 blob 26 | 5d7cd731f9beefea46efe0d13fb1ec11bfb09001 blob 27 | 95c8c8a03c2c037b5de2a1eb80d55ec8dd80a528 blob 28 | cfaa614fd42ee1408341d3db3a1570713ea3494c blob 29 | de9e0026f9d6c1948096dfefb093aa25a188577d blob 30 | e977704a7442a6e80b3d1119a7fcc44b29e22f06 tree 31 | ef2fc0e9ecc83352a410860f6baf8b33c66f82fb tree 32 | f25c461b1674c1d67146f57f7ac9c3626958ff3e blob 33 | f741f48754937179332d2f1fb6f670065c5f69bd commit 34 | f8292a790c79453822afaa6f8fee4dd4a14c5cd1 tree 35 | ff6e0f7a0e941b152ccb63e656b110f48f65515e commit 36 | ``` 37 | -------------------------------------------------------------------------------- /src/_scss/_archive.scss: -------------------------------------------------------------------------------- 1 | #notebook_index { 2 | margin-bottom: 1.2em; 3 | padding-left: 0px; 4 | 5 | li { 6 | list-style-type: none; 7 | 8 | &:before { 9 | content: '\25A0'; 10 | display: block; 11 | position: relative; 12 | max-width: 0; 13 | max-height: 0; 14 | left: -12px; 15 | top: -1px; 16 | color: $primary-color; 17 | font-size: 10px; 18 | } 19 | } 20 | 21 | .notebook_index__tags { 22 | font-size: 75%; 23 | color: $accent-grey; 24 | 25 | a, a:visited { 26 | color: $accent-grey; 27 | margin-left: 0.15rem; 28 | margin-right: 0.15rem; 29 | 30 | &:hover { 31 | color: darken($accent-grey, 25%); 32 | } 33 | } 34 | } 35 | 36 | li:not(:last-child) { 37 | margin-bottom: 0.85em; 38 | } 39 | } 40 | 41 | #notebook_filters { 42 | @include purple_box(); 43 | @include fullwidth_box(); 44 | 45 | display: none; 46 | 47 | .tag_filter { 48 | margin-left: 0.25rem; 49 | margin-right: 0.4rem; 50 | font-weight: bold; 51 | 52 | a.remove_tag { 53 | font-weight: normal; 54 | color: #d01c11; 55 | background-image: none; 56 | padding-left: 3px; 57 | 58 | &::before { 59 | content: "["; 60 | } 61 | 62 | &::after { 63 | content: "]"; 64 | } 65 | 66 | &:hover { 67 | text-decoration: underline; 68 | } 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/_includes/socialgraph.html: -------------------------------------------------------------------------------- 1 | {% if page.theme and page.theme.card_type %} 2 | <meta name="twitter:card" content="{{ page.theme.card_type }}"> 3 | {% else %} 4 | <meta name="twitter:card" content="summary"> 5 | {% endif %} 6 | <meta name="twitter:site" content="@{{ site.social.twitter }}" /> 7 | <meta name="twitter:title" content="{{ page.title }}" /> 8 | {% if page.summary %} 9 | <meta name="twitter:description" content="{{ page.summary }}"> 10 | {% endif %} 11 | {% if page.theme and page.theme.image %} 12 | <meta name="twitter:image" content="{{ site.url }}{{ page.theme.image }}" /> 13 | {% elsif page.theme and page.theme.touch_icon %} 14 | <meta name="twitter:image" content="{{ site.url }}/theme/apple-touch-icon_{{ page.theme.touch_icon }}.png" /> 15 | {% else %} 16 | <meta name="twitter:image" content="{{ site.url }}/theme/apple-touch-icon.png" /> 17 | {% endif %} 18 | 19 | <meta property="og:type" content="article" /> 20 | <meta property="og:url" content="{{ site.url }}{{ page.url }}"> 21 | <meta property="og:title" content="{{ page.title }}"> 22 | {% if page.theme and page.theme.image %} 23 | <meta property="og:image" content="{{ site.url }}{{ page.theme.image }}" /> 24 | {% elsif page.theme and page.theme.touch_icon %} 25 | <meta property="og:image" content="{{ site.url }}/theme/apple-touch-icon_{{ page.theme.touch_icon }}.png" /> 26 | {% else %} 27 | <meta property="og:image" content="{{ site.url }}/theme/apple-touch-icon.png" /> 28 | {% endif %} 29 | {% if page.summary %} 30 | <meta property="og:description" content="{{ page.summary }}" /> 31 | {% endif %} 32 | -------------------------------------------------------------------------------- /src/_scss/_code.scss: -------------------------------------------------------------------------------- 1 | code, pre { 2 | @include purple_box(); 3 | font-family: $mono-font; 4 | overflow-x: auto; 5 | } 6 | 7 | code { 8 | margin: 2px; 9 | padding: 3px 3px; 10 | font-size: 1em * $code-scaling-factor; 11 | 12 | } 13 | 14 | // Disable selecting the $ or the following space in ``console`` code 15 | // blocks. The original space is removed in the `cleanup_text.rb` plugin. 16 | .language-console > pre > code > span.w { 17 | @include disable_select(); 18 | &::after { 19 | content: " "; 20 | } 21 | } 22 | 23 | pre { 24 | @include fullwidth_box(); 25 | 26 | // This ensures that code blocks don't get blown up to big sizes 27 | // on iPhone displays. 28 | -webkit-text-size-adjust: 100%; 29 | 30 | // This ensures the first line of <pre> blocks doesn't have a funny indent 31 | code { 32 | border: none; 33 | background: none; 34 | margin: 0px; 35 | padding-left: 0px; 36 | } 37 | } 38 | 39 | .highlight { 40 | hanging-punctuation: none; 41 | } 42 | 43 | figure.highlight > pre, 44 | figure.highlight > pre > code { 45 | background: none; 46 | border-left: none; 47 | padding: 0px; 48 | } 49 | 50 | figure.highlight > pre { 51 | font-size: 1em; 52 | } 53 | 54 | figure.highlight { 55 | td { 56 | margin: 0px; 57 | padding: 0px; 58 | pre { 59 | margin-top: 0px; 60 | margin-bottom: 0px; 61 | } 62 | } 63 | 64 | pre.lineno { 65 | color: $accent-grey; 66 | @include disable_select(); 67 | } 68 | 69 | td.code { 70 | pre { 71 | border-left: none; 72 | } 73 | width: 100%; 74 | } 75 | } -------------------------------------------------------------------------------- /src/_plugins/github.rb: -------------------------------------------------------------------------------- 1 | require "net/http" 2 | 3 | module Jekyll 4 | class GitHubTag < Liquid::Tag 5 | 6 | def initialize(tag_name, text, tokens) 7 | super 8 | repo, @issue_number = text.strip.split("#") 9 | @owner, @repo = repo.split("/") 10 | end 11 | 12 | def cache_file() 13 | "#{@src}/_github.json" 14 | end 15 | 16 | def render(context) 17 | site = context.registers[:site] 18 | @src = site.config["source"] 19 | 20 | if File.exists? cache_file() 21 | github_data = JSON.parse(File.read(cache_file())) 22 | else 23 | github_data = {} 24 | end 25 | 26 | identifier = "#{@owner}/#{@repo}##{@issue_number}" 27 | 28 | if !github_data.key?(identifier) 29 | uri = URI("https://api.github.com/repos/#{@owner}/#{@repo}/issues/#{@issue_number}") 30 | 31 | req = Net::HTTP::Get.new(uri) 32 | req["Accept"] = "application/vnd.github.v3+json" 33 | http = Net::HTTP.new(uri.host, uri.port) 34 | http.use_ssl = (uri.scheme == "https") 35 | 36 | resp = http.request(req) 37 | 38 | title = JSON.parse(resp.body)["title"] 39 | 40 | github_data[identifier] = { 41 | "title" => title 42 | } 43 | 44 | json_string = JSON.pretty_generate(github_data) 45 | File.open(cache_file(), "w") { |f| f.write(json_string) } 46 | end 47 | 48 | issue_data = github_data[identifier] 49 | 50 | "<a href=\"https://github.com/#{@owner}/#{@repo}/issues/#{@issue_number}\">#{identifier}</a> – #{issue_data['title']}" 51 | end 52 | end 53 | end 54 | 55 | Liquid::Template.register_tag('github_issue', Jekyll::GitHubTag) 56 | -------------------------------------------------------------------------------- /src/_posts/2019/2019-06-02-secret-files-in-azure-pipelines.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: post 3 | title: "Storing secret files in Azure Pipelines" 4 | date: 2019-06-02 17:03:07 +0100 5 | tags: azure-pipelines 6 | --- 7 | 8 | Relevant docs: 9 | 10 | - [Secure files for Azure Pipelines](https://docs.microsoft.com/en-us/azure/devops/pipelines/library/secure-files) 11 | - [Download Secure File task](https://docs.microsoft.com/en-us/azure/devops/pipelines/tasks/utility/download-secure-file?view=azure-devops) 12 | 13 | Steps: 14 | 15 | 1. From the front page of Azure, go [the "Library tab"](https://dev.azure.com/alexwlchan/alexwlchan/_library?itemType=VariableGroups) and select ["Secure Files"](https://dev.azure.com/alexwlchan/alexwlchan/_library?itemType=SecureFiles). 16 | 17 | <img src="/images/azure-pipelines-library.jpg"> 18 | 19 | 2. Upload the secret file you want to use in your builds. 20 | 21 | <img src="/images/azure-pipelines-secret-files.jpg"> 22 | 23 | 3. Add the following task to `azure-pipelines.yml`: 24 | 25 | ```yaml 26 | - task: DownloadSecureFile@1 27 | inputs: 28 | secureFile: {filename} 29 | ``` 30 | 31 | where `{filename}` is the name of the file uploaded in step 2. 32 | 33 | 4. Kick off a build. 34 | This will fail with an error in the "Download Secure File" stage. 35 | In the Pipelines console, go through and click “Authorize resources”. 36 | 37 | <img src="/images/azure-pipelines-permissions-error.jpg"> 38 | 39 | 5. Rerun the build. 40 | It should decrypt now! 41 | 42 | 6. Access the file path in a script block as follows: 43 | 44 | ```shell 45 | ssh -i DOWNLOADSECUREFILE_SECUREFILEPATH alexwlchan@my_linode.dev 46 | ``` 47 | -------------------------------------------------------------------------------- /src/_includes/head.html: -------------------------------------------------------------------------------- 1 | <head> 2 | <meta charset="utf-8"> 3 | <meta http-equiv="X-UA-Compatible" content="IE=edge"> 4 | <meta name="viewport" content="width=device-width, initial-scale=1"> 5 | 6 | 7 | <title>{% if page.title and page.title != site.title %}{{ page.title | strip_html | smartify }} – {% endif %}{{ site.title | smartify }} 8 | 9 | 10 | 11 | 12 | 13 | {% if page.theme and page.theme.color %} 14 | 15 | 16 | 17 | {% else %} 18 | 19 | 20 | 21 | {% endif %} 22 | 23 | {% if page.theme and page.theme.touch_icon %} 24 | 25 | {% else %} 26 | 27 | {% endif %} 28 | 29 | {% include socialgraph.html %} 30 | 31 | 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /src/_plugins/tag_cloud.rb: -------------------------------------------------------------------------------- 1 | module Jekyll 2 | module TagCloudFilter 3 | def build_tag_cloud(input) 4 | weights = Hash[input.map { 5 | |tag_name, posts| [tag_name, posts.size] 6 | }] 7 | weight_min = weights.values.min 8 | weight_max = weights.values.max 9 | 10 | weight_range = weight_max - weight_min 11 | if weight_range == 0 12 | weight_range = 1 13 | end 14 | 15 | # These values are hard-coded for now. 16 | # TODO: Put them in settings! 17 | size_min = 10 18 | size_max = 28 19 | 20 | color_min = "#999999" 21 | color_max = "#7253ed" 22 | 23 | red = {"min" => color_min[1..2].to_i(16), "max" => color_max[1..2].to_i(16)} 24 | green = {"min" => color_min[3..4].to_i(16), "max" => color_max[3..4].to_i(16)} 25 | blue = {"min" => color_min[5..6].to_i(16), "max" => color_max[5..6].to_i(16)} 26 | 27 | # Remember to use .to_f to get precise answers; Ruby does int division 28 | # by default. 29 | size_increment = (size_max - size_min) / weight_range.to_f 30 | red_increment = (red["max"] - red["min"]) / weight_range.to_f 31 | green_increment = (green["max"] - green["min"]) / weight_range.to_f 32 | blue_increment = (blue["max"] - blue["min"]) / weight_range.to_f 33 | 34 | Hash[weights.map { 35 | |tag_name, post_count| [tag_name, 36 | { 37 | "size" => (size_min + post_count * size_increment).to_i, 38 | "red" => "%02x" % [(red["min"] + post_count * red_increment), 255].min, 39 | "green" => "%02x" % [(green["min"] + post_count * green_increment), 255].min, 40 | "blue" => "%02x" % [(blue["min"] + post_count * blue_increment), 255].min, 41 | } 42 | ] 43 | } 44 | ] 45 | end 46 | end 47 | end 48 | 49 | Liquid::Template::register_filter(Jekyll::TagCloudFilter) 50 | -------------------------------------------------------------------------------- /src/_includes/post_content.html: -------------------------------------------------------------------------------- 1 |
2 |

3 | {% if post.link %} 4 | 5 | {% else %} 6 | 7 | {% endif %} 8 | {{ post.title | cleanup_text | smartify }} 9 |

10 | 11 | 35 | 36 | {% assign is_index = is_index | default: false %} 37 | 38 | {% if post.content_warning %} 39 |

Content warning: {{ post.content_warning }}

40 | {% endif %} 41 | 42 | {% if is_index and post.content contains '' %} 43 | {% assign splitcontent = post.content | split: '' %} 44 | {{ splitcontent.first | cleanup_text | smartify }} 45 |

Read more →

46 | {% else %} 47 | {{ post.content | cleanup_text | smartify }} 48 | {% endif %} 49 |
50 | -------------------------------------------------------------------------------- /src/_posts/2019/2019-05-09-scanamo-conditional-updates-on-nested-fields.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: post 3 | title: "DynamoDB: Conditional updates on nested fields" 4 | date: 2019-05-09 12:12:24 +0100 5 | tags: java aws aws:dynamodb scala scala:scanamo 6 | --- 7 | 8 | I always struggle with the ConditionalUpdate syntax for DynamoDB, so here's a snippet for the Java SDK that works (with a bit of Scanamo magic to turn case class instances into instances of `AttributeValue`): 9 | 10 | ```scala 11 | import com.gu.scanamo.DynamoFormat 12 | import com.amazonaws.services.dynamodbv2.model.{ 13 | AttributeValue, 14 | UpdateItemRequest 15 | } 16 | 17 | import scala.collection.JavaConverters._ 18 | 19 | case class Versioned[T](payload: T, version: Version) 20 | 21 | val value: Versioned[T] 22 | 23 | val evidenceV: DynamoFormat[Version] 24 | val evidenceT: DynamoFormat[T] 25 | 26 | val versionAv: AttributeValue = evidenceV.write(value.version) 27 | val payloadAv: AttributeValue = evidenceT.write(value.payload) 28 | 29 | val updateItemRequest = new UpdateItemRequest() 30 | .withTableName(dynamoConfig.table) 31 | .addKeyEntry("id", new AttributeValue().withS(value.version.id)) 32 | .withUpdateExpression( 33 | "SET payload = :payload, version = :version" 34 | ) 35 | .withConditionExpression("attribute_not_exists(id) OR version < :currentVersion") 36 | .withExpressionAttributeValues( 37 | Map( 38 | ":currentVersion" -> new AttributeValue(value.version.version.toString), 39 | ":payload" -> payloadAv, 40 | ":version" -> versionAv 41 | ).asJava 42 | ) 43 | ``` 44 | 45 | Here is the equivalent code in the Scanamo DSL: 46 | 47 | ```scala 48 | val ops = table 49 | .given( 50 | not(attributeExists('id)) or 51 | (attributeExists('id) and 'version \ 'version < value.version.version) 52 | ) 53 | .update( 54 | 'id -> value.version.id, 55 | set('version -> value.version) and set('payload -> value.payload) 56 | ) 57 | 58 | Scanamo.exec(dynamoClient)(ops) 59 | ``` 60 | 61 | Note especially the use of `'version \ 'version` to access the nested field. 62 | 63 | References: 64 | 65 | - {% github_issue scanamo/scanamo#136 %} 66 | -------------------------------------------------------------------------------- /src/_plugins/theming.rb: -------------------------------------------------------------------------------- 1 | Jekyll::Hooks.register :site, :post_read do |site| 2 | src = site.config["source"] 3 | site.posts.docs.each { |post| 4 | if post["theme"] && post["theme"]["color"] 5 | color = post["theme"]["color"] 6 | create_scss_theme(src, color) 7 | create_banner_image(src, color) 8 | end 9 | } 10 | end 11 | 12 | 13 | # Create an SCSS theme with this color as the $primary-color variable. 14 | # 15 | # This will be picked up by the SCSS processor for the site, and cause 16 | # the creation of a CSS theme with this as the primary color. 17 | def create_scss_theme(src, color) 18 | mainfile = "#{src}/theme/style_#{color}.scss" 19 | if ! File.file?(mainfile) 20 | File.open(mainfile, 'w') { |file| file.write(<<-EOT 21 | --- 22 | --- 23 | 24 | $primary-color: ##{color}; 25 | 26 | @import "_main.scss"; 27 | EOT 28 | ) } 29 | puts(mainfile) 30 | end 31 | end 32 | 33 | 34 | # Create a Specktre-based banner image with this color as the primary color. 35 | # 36 | # This will be picked up by the rsync plugin, and used by the CSS theme 37 | # created above. 38 | def create_banner_image(src, color) 39 | banner_file = "#{src}/theme/specktre_#{color}.png" 40 | if ! File.file?(banner_file) 41 | 42 | puts("Building the banner file for #{color}") 43 | 44 | red = (color.to_i(16) >> 16) % 256 45 | green = (color.to_i(16) >> 8) % 256 46 | blue = (color.to_i(16) % 256) 47 | 48 | start_color = ( 49 | red * 0.9 * 65536 + 50 | green * 0.9 * 256 + 51 | blue * 0.9).to_i() 52 | end_color = ( 53 | [red * 1.05, 255].min.to_i() * 65536 + 54 | [green * 1.05, 255].min.to_i() * 256 + 55 | [blue * 1.05, 255].min.to_i()) 56 | 57 | start_color = "%06x" % start_color 58 | end_color = "%06x" % end_color 59 | 60 | if !system("specktre new --size=3000x250 --start=#{start_color} --end=#{end_color} --squares --name=#{banner_file}") 61 | raise RuntimeError, "Error running the Specktre process for #{color}!" 62 | end 63 | 64 | if !system("optipng #{banner_file}") 65 | raise RuntimeError, "Error running optipng on #{banner_file}" 66 | end 67 | end 68 | end 69 | -------------------------------------------------------------------------------- /src/_posts/2018/2018-07-17-beware-the-order-of-inheritance-in-scala.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: post 3 | title: Beware the order of inheritance in Scala 4 | date: 2018-07-17 23:27:06 +0100 5 | tags: scala 6 | --- 7 | 8 | Context: 9 | 10 | We used to have a monorepo, but build times led us to remove the storage library and ship it separately. I’m migrating the repo to use the external lib, and exactly one of the tests is throwing a NullPointerException for an unknown reason. 11 | 12 | Cue much cursing and debugging… 13 | 14 | The lesson here is “the order in which you extend traits may be significant”. Commit message reproduced below. 15 | 16 | --- 17 | 18 | [platform#c546f2a](https://github.com/wellcometrust/platform/pull/2428/commits/c546f2ad17c33df83b45184ca23c389ee3dc58f4) 19 | 20 | Why inheritance is evil, ingestor edition 21 | 22 | This test was throwing a NullPointerException when run with the new 23 | storage library, which is pretty annoying and unhelpful. The exception 24 | came from the `eventually` block that checks if the Elasticsearch 25 | container has started, which hadn't changed. Hmm. 26 | 27 | I removed all the test cases, and it wasn't anything to do with them. 28 | Then I started removing the traits we extend from (I'd call them mixins 29 | in Python, but I don't know what they're called in Scala). The only 30 | difference came when I removed both `Messaging` and `S3`, which makes 31 | more sense because at least the `S3` trait has changed. 32 | 33 | Digging another level down, note that the `S3` trait has the 34 | `ExtendedPatience` trait, which sets some patience configuration for 35 | `eventually` blocks. If you remove that, the problem goes away. 36 | 37 | And then it hits me. 38 | 39 | When we removed the storage library, we created a second copy of 40 | `ExtendedPatience` -- and the two are somehow treading on each other's 41 | toes, and throwing this (unhelpful) error. Even though they contain 42 | identical configuration. 43 | 44 | This isn't the only test that has both instances of `ExtendedPatience`, 45 | so what's different? 46 | 47 | Hmm, the order in which the traits are extended is different... and 48 | therein lies the bug. 49 | 50 | I hate computers. 51 | -------------------------------------------------------------------------------- /src/_posts/2018/2018-11-10-some-notes-on-using-typesafe-for-config.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: post 3 | title: Using Typesafe for config in Scala apps 4 | date: 2018-11-10 16:59:30 +0000 5 | tags: scala 6 | --- 7 | 8 | I've just been doing a bunch of work (see [wellcometrust/platform#2967](https://github.com/wellcometrust/platform/issues/2967) and related patches) to use Typesafe for config in our Scala apps, and ditch our use of Guice/Finatra and dependency injection. 9 | 10 | Disentangling the key bits was moderately fiddly, so these are notes on how I got it working. 11 | 12 | 1. Config file. 13 | An example is easiest: 14 | 15 | ```ini 16 | app.contextURL="https://example.org/context.json" 17 | app.namespace=notifier 18 | ``` 19 | 20 | Note that strings have to be quoted, or the Typesafe parser things they're part of the format. 21 | 22 | 2. Pass the path to the config file with the `-Dconfig.file` flag. 23 | For example: 24 | 25 | ```console 26 | $ /opt/docker/bin/application -Dconfig.file=/opt/docker/conf/application.conf 27 | ``` 28 | 29 | 3. Load the config using the factory: 30 | 31 | ```scala 32 | import com.typesafe.config.{Config, ConfigFactory} 33 | 34 | object Main extends App { 35 | val config: Config = ConfigFactory.load() 36 | 37 | ... 38 | } 39 | ``` 40 | 41 | 4. Add the helper class for reading config flags: 42 | 43 | ```scala 44 | import com.typesafe.config.Config 45 | 46 | object EnrichConfig { 47 | implicit class RichConfig(val underlying: Config) extends AnyVal { 48 | def get[T](path: String): Option[T] = 49 | if (underlying.hasPath(path)) { 50 | Some(underlying.getAnyRef(path).asInstanceOf[T]) 51 | } else { 52 | None 53 | } 54 | 55 | def required[T](path: String): T = 56 | get(path).getOrElse { 57 | throw new RuntimeException(s"No value found for path $path") 58 | } 59 | 60 | def getOrElse[T](path: String)(default: T): T = 61 | get(path).getOrElse(default) 62 | } 63 | } 64 | ``` 65 | 66 | 5. Read values from the config. 67 | For example: 68 | 69 | ```scala 70 | import EnrichConfig._ 71 | 72 | object Main extends App { 73 | ... 74 | 75 | val namespace: String = config 76 | .required[String]("app.namespace") 77 | 78 | val contextUrl = config 79 | .getOrElse[String]("app.contextURL")(default = "context.json") 80 | 81 | val waitTime: Option[Int] = config 82 | .get[Int]("app.startUpTime")(default = 10) 83 | } 84 | ``` -------------------------------------------------------------------------------- /src/static/notebook.js: -------------------------------------------------------------------------------- 1 | function filterToTag(t) { 2 | li_tags = document.getElementById("notebook_index").getElementsByTagName("li"); 3 | for (i = 0; i < li_tags.length; i++) { 4 | item = li_tags.item(i); 5 | 6 | if (item.classList.contains("tagged_with_" + t)) { 7 | continue; 8 | } 9 | 10 | if (item.classList.contains("not_tagged_with_" + t)) { 11 | continue; 12 | } 13 | 14 | item.classList.add("not_tagged_with_" + t); 15 | item.style.display = "none"; 16 | } 17 | 18 | url = new URL(window.location.href); 19 | if (!url.searchParams.getAll("tag").includes(t)) { 20 | url.searchParams.append("tag", t); 21 | window.history.pushState({path: url.href}, "", url.href); 22 | } 23 | 24 | filters = document.getElementById("notebook_filters"); 25 | filters.style.display = "block"; 26 | 27 | if (document.getElementById("tag_filter__" + t) === null) { 28 | filters.innerHTML += '' + t + 'x'; 29 | } 30 | } 31 | 32 | function isUntagged(classList) { 33 | for (i = 0; i < classList.length; i++) { 34 | if (classList[i].startsWith("not_tagged_with_")) { 35 | return false; 36 | } 37 | } 38 | return true; 39 | } 40 | 41 | function unfilterTag(t) { 42 | li_tags = document.getElementById("notebook_index").getElementsByTagName("li"); 43 | for (i = 0; i < li_tags.length; i++) { 44 | item = li_tags.item(i); 45 | 46 | if (!item.classList.contains("not_tagged_with_" + t)) { 47 | continue; 48 | } 49 | 50 | item.classList.remove("not_tagged_with_" + t); 51 | 52 | if (isUntagged(item.classList)) { 53 | item.style.display = "list-item"; 54 | } 55 | } 56 | 57 | document.getElementById("tag_filter__" + t).remove(); 58 | 59 | var url = new URL(window.location.href); 60 | existingTags = url.searchParams.getAll("tag"); 61 | url.searchParams.delete("tag"); 62 | for (i = 0; i < existingTags.length; i++) { 63 | if (existingTags[i] !== t) { 64 | url.searchParams.append("tag", existingTags[i]); 65 | } 66 | } 67 | window.history.pushState({path: url.href}, "", url.href); 68 | 69 | if (url.searchParams.getAll("tag").length == 0) { 70 | document.getElementById("notebook_filters").style.display = "none"; 71 | } 72 | } 73 | 74 | window.onload = function() { 75 | params = new URLSearchParams(window.location.search); 76 | 77 | tags = params.getAll("tag"); 78 | for (i = 0; i < tags.length; i++) { 79 | filterToTag(tags[i]); 80 | } 81 | 82 | filtered_tags = tags; 83 | } 84 | -------------------------------------------------------------------------------- /src/_scss/_text.scss: -------------------------------------------------------------------------------- 1 | /// This file contains basic styles for body text on the site. 2 | /// 3 | /// It applies across both pages and posts. This file should not contain 4 | /// styles about page layout or positioning. 5 | 6 | body { 7 | font-family: $main-font-family; 8 | line-height: $line-height; 9 | color: $body-color; 10 | hanging-punctuation: first; 11 | } 12 | 13 | a { 14 | color: $primary-color; 15 | text-decoration: none; 16 | background-image: linear-gradient(#eeebee 60%, #eeebee 100%); 17 | background-repeat: repeat-x; 18 | background-position: 0 100%; 19 | background-size: 1px 1px; 20 | 21 | &:hover { 22 | background-image: linear-gradient(rgba(114,83,237,0.45) 0%, rgba(114,83,237,0.45) 100%); 23 | background-size: 1px 1px 24 | } 25 | 26 | &:not([class]):visited { 27 | color: $primary-dark; 28 | } 29 | } 30 | 31 | .title a { 32 | background-image: none; 33 | } 34 | 35 | h1 { 36 | color: $primary-color; 37 | } 38 | 39 | h2 { 40 | line-height: $line-height; 41 | font-weight: normal; 42 | margin-top: 2em; 43 | } 44 | 45 | .post__separator + h2 { 46 | margin-top: 0em; 47 | } 48 | 49 | blockquote { 50 | 51 | padding: ($default-padding / 2) ($default-padding * 0.6 - $sidebar-border-width); 52 | line-height: $line-height * $code-scaling-factor * 1.08; 53 | 54 | margin-left: -$default-padding * 0.6; 55 | margin-right: -$default-padding * 0.6; 56 | 57 | background-color: rgba(114, 83, 237, 0.04); 58 | 59 | border: $sidebar-border-width solid rgba(114, 83, 237, 0.45); 60 | border-radius: 5px; 61 | 62 | p:first-child { 63 | margin-top: 0; 64 | } 65 | 66 | p:last-child { 67 | margin-bottom: 0; 68 | } 69 | 70 | // border-left: $sidebar-border-width solid $midtone-gray; 71 | // margin-left: -$default-padding; 72 | // margin-right: 0px; 73 | // padding: 0px ($default-padding - $sidebar-border-width); 74 | // 75 | // & > p { 76 | //// font-style: italic; 77 | //// color: #6f6f6f; 78 | // } 79 | 80 | @media print { 81 | color: $body-color; 82 | } 83 | 84 | pre { 85 | border-color: #aaa; 86 | } 87 | 88 | em { 89 | font-style: normal; 90 | } 91 | 92 | @media screen and (max-width: $max-width + $default-padding * 3) { 93 | margin-left: 0px; 94 | } 95 | } 96 | 97 | // These rules help with the positioning of footnote markers, although 98 | // I'm not entirely sure how they work. 99 | sup, sub { 100 | vertical-align: 0ex; 101 | position: relative; 102 | } 103 | 104 | sub { 105 | top: 0.8ex; 106 | } 107 | 108 | sup { 109 | bottom: 1ex; 110 | } 111 | 112 | .footnotes { 113 | font-size: 1em * $scaling-factor; 114 | } 115 | 116 | .title { 117 | font-size: 1.9em; 118 | font-weight: normal; 119 | line-height: 1.45em; 120 | a { 121 | text-decoration: none; 122 | } 123 | &.linkpost_title, 124 | &.minipost_title { 125 | font-size: 1.17em; 126 | line-height: 1.35em; 127 | padding-top: 12px; 128 | } 129 | &.linkpost_title { 130 | a { 131 | text-decoration: underline; 132 | &:hover { 133 | text-decoration: none; 134 | } 135 | } 136 | &::after { 137 | content: "\a0→"; 138 | color: $accent-grey; 139 | } 140 | } 141 | a:visited { 142 | color: $primary-color; 143 | } 144 | margin-bottom: -6px; 145 | } 146 | 147 | .content_warning { 148 | font-style: italic; 149 | } 150 | 151 | h2, h3 { 152 | &:hover { 153 | & > a.anchor { 154 | display: inline-block; 155 | } 156 | } 157 | } 158 | 159 | a.anchor { 160 | &::after { 161 | content: "¶"; 162 | } 163 | 164 | display: none; 165 | text-decoration: none; 166 | 167 | &, &:visited { 168 | color: $accent-grey; 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /src/_scss/_pygments.scss: -------------------------------------------------------------------------------- 1 | // Code highlighting styles for Pygments. 2 | // 3 | // The bulk of this file was obtained by running: 4 | // 5 | // pygmentize -S default -f html 6 | // 7 | // and pasting the output into this file. 8 | 9 | pre { 10 | .hll { background-color: #ffffcc } 11 | .c { color: #408080; font-style: italic } /* Comment */ 12 | .err { border: 1px solid #FF0000 } /* Error */ 13 | .k { color: #008000; font-weight: bold } /* Keyword */ 14 | .o { color: #666666 } /* Operator */ 15 | .ch { color: #408080; font-style: italic } /* Comment.Hashbang */ 16 | .cm { color: #408080; font-style: italic } /* Comment.Multiline */ 17 | .cp { color: #BC7A00 } /* Comment.Preproc */ 18 | .cpf { color: #408080; font-style: italic } /* Comment.PreprocFile */ 19 | .c1 { color: #408080; font-style: italic } /* Comment.Single */ 20 | .cs { color: #408080; font-style: italic } /* Comment.Special */ 21 | .gd { color: #A00000 } /* Generic.Deleted */ 22 | .ge { font-style: italic } /* Generic.Emph */ 23 | .gr { color: #FF0000 } /* Generic.Error */ 24 | .gh { color: #000080; font-weight: bold } /* Generic.Heading */ 25 | .gi { color: #00A000 } /* Generic.Inserted */ 26 | .go { color: #888888 } /* Generic.Output */ 27 | .gp { color: #000080; font-weight: bold } /* Generic.Prompt */ 28 | .gs { font-weight: bold } /* Generic.Strong */ 29 | .gu { color: #800080; font-weight: bold } /* Generic.Subheading */ 30 | .gt { color: #0044DD } /* Generic.Traceback */ 31 | .kc { color: #008000; font-weight: bold } /* Keyword.Constant */ 32 | .kd { color: #008000; font-weight: bold } /* Keyword.Declaration */ 33 | .kn { color: #008000; font-weight: bold } /* Keyword.Namespace */ 34 | .kp { color: #008000 } /* Keyword.Pseudo */ 35 | .kr { color: #008000; font-weight: bold } /* Keyword.Reserved */ 36 | .kt { color: #B00040 } /* Keyword.Type */ 37 | .m { color: #666666 } /* Literal.Number */ 38 | .s { color: #BA2121 } /* Literal.String */ 39 | .na { color: #7D9029 } /* Name.Attribute */ 40 | .nb { color: #008000 } /* Name.Builtin */ 41 | .nc { color: #0000FF; font-weight: bold } /* Name.Class */ 42 | .no { color: #880000 } /* Name.Constant */ 43 | .nd { color: #AA22FF } /* Name.Decorator */ 44 | .ni { color: #999999; font-weight: bold } /* Name.Entity */ 45 | .ne { color: #D2413A; font-weight: bold } /* Name.Exception */ 46 | .nf { color: #0000FF } /* Name.Function */ 47 | .nl { color: #A0A000 } /* Name.Label */ 48 | .nn { color: #0000FF; font-weight: bold } /* Name.Namespace */ 49 | .nt { color: #008000; font-weight: bold } /* Name.Tag */ 50 | .nv { color: #19177C } /* Name.Variable */ 51 | .ow { color: #AA22FF; font-weight: bold } /* Operator.Word */ 52 | .w { color: #bbbbbb } /* Text.Whitespace */ 53 | .mb { color: #666666 } /* Literal.Number.Bin */ 54 | .mf { color: #666666 } /* Literal.Number.Float */ 55 | .mh { color: #666666 } /* Literal.Number.Hex */ 56 | .mi { color: #666666 } /* Literal.Number.Integer */ 57 | .mo { color: #666666 } /* Literal.Number.Oct */ 58 | .sa { color: #BA2121 } /* Literal.String.Affix */ 59 | .sb { color: #BA2121 } /* Literal.String.Backtick */ 60 | .sc { color: #BA2121 } /* Literal.String.Char */ 61 | .dl { color: #BA2121 } /* Literal.String.Delimiter */ 62 | .sd { color: #BA2121; font-style: italic } /* Literal.String.Doc */ 63 | .s2 { color: #BA2121 } /* Literal.String.Double */ 64 | .se { color: #BB6622; font-weight: bold } /* Literal.String.Escape */ 65 | .sh { color: #BA2121 } /* Literal.String.Heredoc */ 66 | .si { color: #BB6688; font-weight: bold } /* Literal.String.Interpol */ 67 | .sx { color: #008000 } /* Literal.String.Other */ 68 | .sr { color: #BB6688 } /* Literal.String.Regex */ 69 | .s1 { color: #BA2121 } /* Literal.String.Single */ 70 | .ss { color: #19177C } /* Literal.String.Symbol */ 71 | .bp { color: #008000 } /* Name.Builtin.Pseudo */ 72 | .fm { color: #0000FF } /* Name.Function.Magic */ 73 | .vc { color: #19177C } /* Name.Variable.Class */ 74 | .vg { color: #19177C } /* Name.Variable.Global */ 75 | .vi { color: #19177C } /* Name.Variable.Instance */ 76 | .vm { color: #19177C } /* Name.Variable.Magic */ 77 | .il { color: #666666 } /* Literal.Number.Integer.Long */ 78 | } 79 | -------------------------------------------------------------------------------- /src/_layouts/compress.html: -------------------------------------------------------------------------------- 1 | --- 2 | # Jekyll layout that compresses HTML 3 | # v3.0.2 4 | # http://jch.penibelst.de/ 5 | # © 2014–2015 Anatol Broder 6 | # MIT License 7 | --- 8 | 9 | {% capture _LINE_FEED %} 10 | {% endcapture %}{% if site.compress_html.ignore.envs contains jekyll.environment %}{{ content }}{% else %}{% capture _content %}{{ content }}{% endcapture %}{% assign _profile = site.compress_html.profile %}{% if site.compress_html.endings == "all" %}{% assign _endings = "html head body li dt dd p rt rp optgroup option colgroup caption thead tbody tfoot tr td th" | split: " " %}{% else %}{% assign _endings = site.compress_html.endings %}{% endif %}{% for _element in _endings %}{% capture _end %}{% endcapture %}{% assign _content = _content | remove: _end %}{% endfor %}{% if _profile and _endings %}{% assign _profile_endings = _content | size | plus: 1 %}{% endif %}{% for _element in site.compress_html.startings %}{% capture _start %}<{{ _element }}>{% endcapture %}{% assign _content = _content | remove: _start %}{% endfor %}{% if _profile and site.compress_html.startings %}{% assign _profile_startings = _content | size | plus: 1 %}{% endif %}{% if site.compress_html.comments == "all" %}{% assign _comments = "" | split: " " %}{% else %}{% assign _comments = site.compress_html.comments %}{% endif %}{% if _comments.size == 2 %}{% capture _comment_befores %}.{{ _content }}{% endcapture %}{% assign _comment_befores = _comment_befores | split: _comments.first %}{% for _comment_before in _comment_befores %}{% if forloop.first %}{% continue %}{% endif %}{% capture _comment_outside %}{% if _carry %}{{ _comments.first }}{% endif %}{{ _comment_before }}{% endcapture %}{% capture _comment %}{% unless _carry %}{{ _comments.first }}{% endunless %}{{ _comment_outside | split: _comments.last | first }}{% if _comment_outside contains _comments.last %}{{ _comments.last }}{% assign _carry = false %}{% else %}{% assign _carry = true %}{% endif %}{% endcapture %}{% assign _content = _content | remove_first: _comment %}{% endfor %}{% if _profile %}{% assign _profile_comments = _content | size | plus: 1 %}{% endif %}{% endif %}{% assign _pre_befores = _content | split: "" %}{% assign _pres_after = "" %}{% if _pres.size != 0 %}{% if site.compress_html.blanklines %}{% assign _lines = _pres.last | split: _LINE_FEED %}{% capture _pres_after %}{% for _line in _lines %}{% assign _trimmed = _line | split: " " | join: " " %}{% if _trimmed != empty or forloop.last %}{% unless forloop.first %}{{ _LINE_FEED }}{% endunless %}{{ _line }}{% endif %}{% endfor %}{% endcapture %}{% else %}{% assign _pres_after = _pres.last | split: " " | join: " " %}{% endif %}{% endif %}{% capture _content %}{{ _content }}{% if _pre_before contains "" %}{% endif %}{% unless _pre_before contains "" and _pres.size == 1 %}{{ _pres_after }}{% endunless %}{% endcapture %}{% endfor %}{% if _profile %}{% assign _profile_collapse = _content | size | plus: 1 %}{% endif %}{% if site.compress_html.clippings == "all" %}{% assign _clippings = "html head title base link meta style body article section nav aside h1 h2 h3 h4 h5 h6 hgroup header footer address p hr blockquote ol ul li dl dt dd figure figcaption main div table caption colgroup col tbody thead tfoot tr td th" | split: " " %}{% else %}{% assign _clippings = site.compress_html.clippings %}{% endif %}{% for _element in _clippings %}{% assign _edges = " ;; ;" | replace: "e", _element | split: ";" %}{% assign _content = _content | replace: _edges[0], _edges[1] | replace: _edges[2], _edges[3] | replace: _edges[4], _edges[5] %}{% endfor %}{% if _profile and _clippings %}{% assign _profile_clippings = _content | size | plus: 1 %}{% endif %}{{ _content }}{% if _profile %}
Step Bytes
raw {{ content | size }}{% if _profile_endings %}
endings {{ _profile_endings }}{% endif %}{% if _profile_startings %}
startings {{ _profile_startings }}{% endif %}{% if _profile_comments %}
comments {{ _profile_comments }}{% endif %}{% if _profile_collapse %}
collapse {{ _profile_collapse }}{% endif %}{% if _profile_clippings %}
clippings {{ _profile_clippings }}{% endif %}
{% endif %}{% endif %} 11 | -------------------------------------------------------------------------------- /src/_posts/2018/2018-11-23-notes-from-working-through-the-ruby-koans.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: post 3 | title: Notes from working through the Ruby Koans 4 | date: 2018-11-23 12:01:12 +0000 5 | tags: ruby 6 | --- 7 | 8 | Some notes from when I worked through the [Ruby Koans](http://rubykoans.com/). 9 | Grouped by name of the file where I learnt something. 10 | 11 | about_asserts.rb: 12 | 13 | * The following are equivalent: 14 | 15 | ```ruby 16 | assert foo == bar 17 | assert_equal foo, bar 18 | ``` 19 | 20 | about_nil.rb: 21 | 22 | * `nil` is an Object. 23 | * You can use `foo.nil?` to check if an object is `nil`. 24 | * You can check for string matches with a regex, e.g.: 25 | 26 | ```ruby 27 | assert_match(/undefined method/, ex.message) 28 | ``` 29 | 30 | about_objects.rb 31 | 32 | * The ID of an object is a `Fixnum`. 33 | * All objects have a different object ID. 34 | * Small ints have a fixed object ID. 35 | See [Object IDs in Ruby](http://thepaulrayner.com/blog/2013/02/06/object-ids-in-ruby/) for more details. 36 | 37 | about_arrays.rb 38 | 39 | * There are helper methods `arr.first` and `arr.last`. 40 | * Array slices work differently to Python: the structure is `arr[index, length]`. 41 | If the index is out of range, it returns `nil`. 42 | * Dots work as follows (the opposite of what you expected!): 43 | 44 | ```ruby 45 | a..b # a <= i <= b 46 | a...b # a <= i < b 47 | ``` 48 | 49 | * More convenience methods and their Python equivalents: 50 | 51 | ```ruby 52 | arr.unshift(x) # list.insert(0, x) 53 | arr.shift # list.pop(0) 54 | ``` 55 | 56 | about_hashes.rb 57 | 58 | * Behaviour is opposite to Python: default hash lookup (`h[key]`) returns `nil` if the key doesn't exist. 59 | To get an explicit `KeyError`, you need to use `h.fetch(key)`. 60 | * You can provide default values to get similar behaviour to Python's defaultdict: 61 | 62 | ```ruby 63 | Hash.new(default) 64 | Hash.new { |hash, key| hash[key] = default } 65 | ``` 66 | 67 | Be careful about mutating the default! 68 | In the first line, the same value of `default` is used everywhere, so editing it will affect every key where it's used. 69 | 70 | about_strings.rb: 71 | 72 | * You can be flexible about quoting strings if it's helpful, including multi-line strings. 73 | 74 | ```ruby 75 | %{"I don't have the answer" he said} 76 | %("I don't have the answer" he said) 77 | %!"I don't have the answer" he said! 78 | ``` 79 | 80 | * You also have `<