diff --git a/.github/workflows/homebrew.yml b/.github/workflows/homebrew.yml new file mode 100644 index 00000000..36f68a7e --- /dev/null +++ b/.github/workflows/homebrew.yml @@ -0,0 +1,70 @@ +name: Homebrew Formula + +on: + push: + branches: [ main ] + paths: + - 'Formula/**' + - '.github/workflows/homebrew.yml' + pull_request: + branches: [ main ] + paths: + - 'Formula/**' + - '.github/workflows/homebrew.yml' + workflow_dispatch: + +jobs: + style: + name: brew style + audit + runs-on: macos-latest + steps: + - uses: actions/checkout@v6 + + - name: Set up Homebrew + uses: Homebrew/actions/setup-homebrew@main + + - name: Tap the formula + # `brew audit` only accepts formula *names*, not file paths. Make the + # repo's formula visible to brew by copying it into a synthetic tap. + run: | + tap_dir="$(brew --repository)/Library/Taps/punitarani/homebrew-fli" + mkdir -p "$tap_dir/Formula" + cp "$GITHUB_WORKSPACE/Formula/flights.rb" "$tap_dir/Formula/flights.rb" + + - name: brew style + run: brew style punitarani/fli/flights + + - name: brew audit + # `--new` is intentionally omitted: it enforces homebrew-core admission + # rules (notability, no duplicate names, etc.) that don't apply to a + # tap formula. `--strict` gives us the substantive content checks + # without network dependency. + run: brew audit --strict punitarani/fli/flights + + install: + name: brew install + test + runs-on: macos-latest + needs: style + steps: + - uses: actions/checkout@v6 + + - name: Set up Homebrew + uses: Homebrew/actions/setup-homebrew@main + + - name: Tap the formula + run: | + tap_dir="$(brew --repository)/Library/Taps/punitarani/homebrew-fli" + mkdir -p "$tap_dir/Formula" + cp "$GITHUB_WORKSPACE/Formula/flights.rb" "$tap_dir/Formula/flights.rb" + + - name: brew install --build-from-source + run: brew install --build-from-source --verbose punitarani/fli/flights + + - name: brew test + run: brew test --verbose punitarani/fli/flights + + - name: Verify CLI entry points + run: | + fli --help + test -x "$(brew --prefix)/bin/fli-mcp" + test -x "$(brew --prefix)/bin/fli-mcp-http" diff --git a/Formula/flights.rb b/Formula/flights.rb new file mode 100644 index 00000000..33dd592b --- /dev/null +++ b/Formula/flights.rb @@ -0,0 +1,431 @@ +class Flights < Formula + include Language::Python::Virtualenv + + desc "Programmatic access to Google Flights via the fli CLI" + homepage "https://github.com/punitarani/fli" + url "https://files.pythonhosted.org/packages/49/a4/781bf3eb1cd51a01fe5c69104570cc888e645009522733267ac98fec7f39/flights-0.8.5.tar.gz" + sha256 "fed27974af8320b64529fd5a2fc87f809c42e820e538a1b1ed55e4afd0b73528" + license "MIT" + + livecheck do + url :stable + strategy :pypi + end + + depends_on "rust" => :build + depends_on "libyaml" + depends_on "python@3.13" + + resource "aiofile" do + url "https://files.pythonhosted.org/packages/54/23/8f73eab04d0587131dc1a5d6cceb1ab6b04f39869c62608a4c6e229975c9/aiofile-3.10.0.tar.gz" + sha256 "11be969b1b8ae85dba1a0d8468af2d0f031a20ab4a446176345b8eccb7bcb15d" + end + + resource "annotated-doc" do + url "https://files.pythonhosted.org/packages/57/ba/046ceea27344560984e26a590f90bc7f4a75b06701f653222458922b558c/annotated_doc-0.0.4.tar.gz" + sha256 "fbcda96e87e9c92ad167c2e53839e57503ecfda18804ea28102353485033faa4" + end + + resource "annotated-types" do + url "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz" + sha256 "aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89" + end + + resource "anyio" do + url "https://files.pythonhosted.org/packages/19/14/2c5dd9f512b66549ae92767a9c7b330ae88e1932ca57876909410251fe13/anyio-4.13.0.tar.gz" + sha256 "334b70e641fd2221c1505b3890c69882fe4a2df910cba14d97019b90b24439dc" + end + + resource "attrs" do + url "https://files.pythonhosted.org/packages/9a/8e/82a0fe20a541c03148528be8cac2408564a6c9a0cc7e9171802bc1d26985/attrs-26.1.0.tar.gz" + sha256 "d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32" + end + + resource "authlib" do + url "https://files.pythonhosted.org/packages/36/98/7d93f30d029643c0275dbc0bd6d5a6f670661ee6c9a94d93af7ab4887600/authlib-1.7.2.tar.gz" + sha256 "2cea25fefcd4e7173bdf1372c0afc265c8034b23a8cd5dcb6a9164b826c64231" + end + + resource "babel" do + url "https://files.pythonhosted.org/packages/7d/b2/51899539b6ceeeb420d40ed3cd4b7a40519404f9baf3d4ac99dc413a834b/babel-2.18.0.tar.gz" + sha256 "b80b99a14bd085fcacfa15c9165f651fbb3406e66cc603abf11c5750937c992d" + end + + resource "beartype" do + url "https://files.pythonhosted.org/packages/c7/94/1009e248bbfbab11397abca7193bea6626806be9a327d399810d523a07cb/beartype-0.22.9.tar.gz" + sha256 "8f82b54aa723a2848a56008d18875f91c1db02c32ef6a62319a002e3e25a975f" + end + + resource "cachetools" do + url "https://files.pythonhosted.org/packages/ff/e2/85f227594656000ff4d8adadae91a21f536d4a84c6c716a86bd6685874be/cachetools-7.1.1.tar.gz" + sha256 "27bdf856d68fd3c71c26c01b5edc312124ed427524d1ddb31aa2b7746fe20d4b" + end + + resource "caio" do + url "https://files.pythonhosted.org/packages/92/88/b8527e1b00c1811db339a1df8bd1ae49d146fcea9d6a5c40e3a80aaeb38d/caio-0.9.25.tar.gz" + sha256 "16498e7f81d1d0f5a4c0ad3f2540e65fe25691376e0a5bd367f558067113ed10" + end + + resource "certifi" do + url "https://files.pythonhosted.org/packages/25/ee/6caf7a40c36a1220410afe15a1cc64993a1f864871f698c0f93acb72842a/certifi-2026.4.22.tar.gz" + sha256 "8d455352a37b71bf76a79caa83a3d6c25afee4a385d632127b6afb3963f1c580" + end + + resource "cffi" do + url "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz" + sha256 "44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529" + end + + resource "click" do + url "https://files.pythonhosted.org/packages/bb/63/f9e1ea081ce35720d8b92acde70daaedace594dc93b693c869e0d5910718/click-8.3.3.tar.gz" + sha256 "398329ad4837b2ff7cbe1dd166a4c0f8900c3ca3a218de04466f38f6497f18a2" + end + + resource "cryptography" do + url "https://files.pythonhosted.org/packages/9f/a9/db8f313fdcd85d767d4973515e1db101f9c71f95fced83233de224673757/cryptography-48.0.0.tar.gz" + sha256 "5c3932f4436d1cccb036cb0eaef46e6e2db91035166f1ad6505c3c9d5a635920" + end + + resource "curl-cffi" do + url "https://files.pythonhosted.org/packages/48/5b/89fcfebd3e5e85134147ac99e9f2b2271165fd4d71984fc65da5f17819b7/curl_cffi-0.15.0.tar.gz" + sha256 "ea0c67652bf6893d34ee0f82c944f37e488f6147e9421bef1771cc6545b02ded" + end + + resource "cyclopts" do + url "https://files.pythonhosted.org/packages/a4/c3/d3f095120329616cc364af2bedcffd518d4db18c978f2f6c892d29e6af2f/cyclopts-4.12.0.tar.gz" + sha256 "86bfb5b35cb078decc1cca6c1be41f9a0e6202dc43b4f6056d5cfc6d1f4a69d1" + end + + resource "dnspython" do + url "https://files.pythonhosted.org/packages/8c/8b/57666417c0f90f08bcafa776861060426765fdb422eb10212086fb811d26/dnspython-2.8.0.tar.gz" + sha256 "181d3c6996452cb1189c4046c61599b84a5a86e099562ffde77d26984ff26d0f" + end + + resource "docstring-parser" do + url "https://files.pythonhosted.org/packages/e0/4d/f332313098c1de1b2d2ff91cf2674415cc7cddab2ca1b01ae29774bd5fdf/docstring_parser-0.18.0.tar.gz" + sha256 "292510982205c12b1248696f44959db3cdd1740237a968ea1e2e7a900eeb2015" + end + + resource "docutils" do + url "https://files.pythonhosted.org/packages/ae/b6/03bb70946330e88ffec97aefd3ea75ba575cb2e762061e0e62a213befee8/docutils-0.22.4.tar.gz" + sha256 "4db53b1fde9abecbb74d91230d32ab626d94f6badfc575d6db9194a49df29968" + end + + resource "email-validator" do + url "https://files.pythonhosted.org/packages/f5/22/900cb125c76b7aaa450ce02fd727f452243f2e91a61af068b40adba60ea9/email_validator-2.3.0.tar.gz" + sha256 "9fc05c37f2f6cf439ff414f8fc46d917929974a82244c20eb10231ba60c54426" + end + + resource "exceptiongroup" do + url "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz" + sha256 "8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219" + end + + resource "fastapi" do + url "https://files.pythonhosted.org/packages/5d/45/c130091c2dfa061bbfe3150f2a5091ef1adf149f2a8d2ae769ecaf6e99a2/fastapi-0.136.1.tar.gz" + sha256 "7af665ad7acfa0a3baf8983d393b6b471b9da10ede59c60045f49fbc89a0fa7f" + end + + resource "fastmcp" do + url "https://files.pythonhosted.org/packages/3b/a9/5c5a01b6abd5346bf60b97cfd29e4a86661940c27dd562bfcda07fd03519/fastmcp-3.3.1.tar.gz" + sha256 "979362ea557de42a5f40342563c7e4b236bcc8e7cd192715f50030695d1a71cd" + end + + resource "fastmcp-slim" do + url "https://files.pythonhosted.org/packages/d1/a0/627103e517e1d0d6f1eec633d5662d13e776f01b45ad188e4f5f7478b438/fastmcp_slim-3.3.1.tar.gz" + sha256 "0957835fc59452e143ab2f4b7836d2d2df9b2d9958408edc79ba8b56232b2a88" + end + + resource "griffelib" do + url "https://files.pythonhosted.org/packages/9d/82/74f4a3310cdabfbb10da554c3a672847f1ed33c6f61dd472681ce7f1fe67/griffelib-2.0.2.tar.gz" + sha256 "3cf20b3bc470e83763ffbf236e0076b1211bac1bc67de13daf494640f2de707e" + end + + resource "h11" do + url "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz" + sha256 "4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1" + end + + resource "httpcore" do + url "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz" + sha256 "6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8" + end + + resource "httpx" do + url "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz" + sha256 "75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc" + end + + resource "httpx-sse" do + url "https://files.pythonhosted.org/packages/0f/4c/751061ffa58615a32c31b2d82e8482be8dd4a89154f003147acee90f2be9/httpx_sse-0.4.3.tar.gz" + sha256 "9b1ed0127459a66014aec3c56bebd93da3c1bc8bb6618c8082039a44889a755d" + end + + resource "idna" do + url "https://files.pythonhosted.org/packages/82/77/7b3966d0b9d1d31a36ddf1746926a11dface89a83409bf1483f0237aa758/idna-3.15.tar.gz" + sha256 "ca962446ea538f7092a95e057da437618e886f4d349216d2b1e294abfdb65fdc" + end + + resource "importlib-metadata" do + url "https://files.pythonhosted.org/packages/f3/49/3b30cad09e7771a4982d9975a8cbf64f00d4a1ececb53297f1d9a7be1b10/importlib_metadata-8.7.1.tar.gz" + sha256 "49fef1ae6440c182052f407c8d34a68f72efc36db9ca90dc0113398f2fdde8bb" + end + + resource "jaraco-classes" do + url "https://files.pythonhosted.org/packages/06/c0/ed4a27bc5571b99e3cff68f8a9fa5b56ff7df1c2251cc715a652ddd26402/jaraco.classes-3.4.0.tar.gz" + sha256 "47a024b51d0239c0dd8c8540c6c7f484be3b8fcf0b2d85c13825780d3b3f3acd" + end + + resource "jaraco-context" do + url "https://files.pythonhosted.org/packages/af/50/4763cd07e722bb6285316d390a164bc7e479db9d90daa769f22578f698b4/jaraco_context-6.1.2.tar.gz" + sha256 "f1a6c9d391e661cc5b8d39861ff077a7dc24dc23833ccee564b234b81c82dfe3" + end + + resource "jaraco-functools" do + url "https://files.pythonhosted.org/packages/36/cf/ea4ef2920830dea3f5ab2ea4da6fb67724e6dca80ee2553788c3607243d0/jaraco_functools-4.5.0.tar.gz" + sha256 "3bb5665ea4a020cf78a7040e89154c77edadb3ca74f366479669c5999aa70b03" + end + + resource "jeepney" do + url "https://files.pythonhosted.org/packages/7b/6f/357efd7602486741aa73ffc0617fb310a29b588ed0fd69c2399acbb85b0c/jeepney-0.9.0.tar.gz" + sha256 "cf0e9e845622b81e4a28df94c40345400256ec608d0e55bb8a3feaa9163f5732" + end + + resource "joserfc" do + url "https://files.pythonhosted.org/packages/3b/dc/5f768c2e391e9afabe5d18e3221346deb5fb6338565f1ccc9e7c6d7befdd/joserfc-1.6.5.tar.gz" + sha256 "1482a7db78fb4602e44ed89e51b599d052e091288c7c532c5b694e20149dec48" + end + + resource "jsonref" do + url "https://files.pythonhosted.org/packages/aa/0d/c1f3277e90ccdb50d33ed5ba1ec5b3f0a242ed8c1b1a85d3afeb68464dca/jsonref-1.1.0.tar.gz" + sha256 "32fe8e1d85af0fdefbebce950af85590b22b60f9e95443176adbde4e1ecea552" + end + + resource "jsonschema" do + url "https://files.pythonhosted.org/packages/b3/fc/e067678238fa451312d4c62bf6e6cf5ec56375422aee02f9cb5f909b3047/jsonschema-4.26.0.tar.gz" + sha256 "0c26707e2efad8aa1bfc5b7ce170f3fccc2e4918ff85989ba9ffa9facb2be326" + end + + resource "jsonschema-path" do + url "https://files.pythonhosted.org/packages/01/86/cfee6dd25843bec0760f456599a4f7e7e40221a934b9229fda0662c859bc/jsonschema_path-0.4.6.tar.gz" + sha256 "c89eb635f4d497c9ac328eeff359c489755838806a7d033510a692e9576f5c4b" + end + + resource "jsonschema-specifications" do + url "https://files.pythonhosted.org/packages/19/74/a633ee74eb36c44aa6d1095e7cc5569bebf04342ee146178e2d36600708b/jsonschema_specifications-2025.9.1.tar.gz" + sha256 "b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d" + end + + resource "keyring" do + url "https://files.pythonhosted.org/packages/43/4b/674af6ef2f97d56f0ab5153bf0bfa28ccb6c3ed4d1babf4305449668807b/keyring-25.7.0.tar.gz" + sha256 "fe01bd85eb3f8fb3dd0405defdeac9a5b4f6f0439edbb3149577f244a2e8245b" + end + + resource "markdown-it-py" do + url "https://files.pythonhosted.org/packages/06/ff/7841249c247aa650a76b9ee4bbaeae59370dc8bfd2f6c01f3630c35eb134/markdown_it_py-4.2.0.tar.gz" + sha256 "04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49" + end + + resource "mcp" do + url "https://files.pythonhosted.org/packages/38/83/d1efe7c2980d8a3afa476f4e3d42d53dd54c0ab94c27bee5d755b45c8b73/mcp-1.27.1.tar.gz" + sha256 "0f47e1820f8f8f941466b39749eb1d1839a04caddca2bc60e9d46e8a99914924" + end + + resource "mdurl" do + url "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz" + sha256 "bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba" + end + + resource "more-itertools" do + url "https://files.pythonhosted.org/packages/a2/f7/139d22fef48ac78127d18e01d80cf1be40236ae489769d17f35c3d425293/more_itertools-11.0.2.tar.gz" + sha256 "392a9e1e362cbc106a2457d37cabf9b36e5e12efd4ebff1654630e76597df804" + end + + resource "openapi-pydantic" do + url "https://files.pythonhosted.org/packages/02/2e/58d83848dd1a79cb92ed8e63f6ba901ca282c5f09d04af9423ec26c56fd7/openapi_pydantic-0.5.1.tar.gz" + sha256 "ff6835af6bde7a459fb93eb93bb92b8749b754fc6e51b2f1590a19dc3005ee0d" + end + + resource "opentelemetry-api" do + url "https://files.pythonhosted.org/packages/fa/fc/b7564cbef36601aef0d6c9bc01f7badb64be8e862c2e1c3c5c3b43b53e4f/opentelemetry_api-1.41.1.tar.gz" + sha256 "0ad1814d73b875f84494387dae86ce0b12c68556331ce6ce8fe789197c949621" + end + + resource "packaging" do + url "https://files.pythonhosted.org/packages/d7/f1/e7a6dd94a8d4a5626c03e4e99c87f241ba9e350cd9e6d75123f992427270/packaging-26.2.tar.gz" + sha256 "ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661" + end + + resource "pathable" do + url "https://files.pythonhosted.org/packages/72/55/b748445cb4ea6b125626f15379be7c96d1035d4fa3e8fee362fa92298abf/pathable-0.5.0.tar.gz" + sha256 "d81938348a1cacb525e7c75166270644782c0fb9c8cecc16be033e71427e0ef1" + end + + resource "platformdirs" do + url "https://files.pythonhosted.org/packages/9f/4a/0883b8e3802965322523f0b200ecf33d31f10991d0401162f4b23c698b42/platformdirs-4.9.6.tar.gz" + sha256 "3bfa75b0ad0db84096ae777218481852c0ebc6c727b3168c1b9e0118e458cf0a" + end + + resource "plotext" do + url "https://files.pythonhosted.org/packages/c9/d7/f75f397af966fe252d0d34ffd3cae765317fce2134f925f95e7d6725d1ce/plotext-5.3.2.tar.gz" + sha256 "52d1e932e67c177bf357a3f0fe6ce14d1a96f7f7d5679d7b455b929df517068e" + end + + resource "py-key-value-aio" do + url "https://files.pythonhosted.org/packages/04/3c/0397c072a38d4bc580994b42e0c90c5f44f679303489e4376289534735e5/py_key_value_aio-0.4.4.tar.gz" + sha256 "e3012e6243ed7cc09bb05457bd4d03b1ba5c2b1ca8700096b3927db79ffbbe55" + end + + resource "pycparser" do + url "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz" + sha256 "600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29" + end + + resource "pydantic" do + url "https://files.pythonhosted.org/packages/18/a5/b60d21ac674192f8ab0ba4e9fd860690f9b4a6e51ca5df118733b487d8d6/pydantic-2.13.4.tar.gz" + sha256 "c40756b57adaa8b1efeeced5c196f3f3b7c435f90e84ea7f443901bec8099ef6" + end + + resource "pydantic-core" do + url "https://files.pythonhosted.org/packages/9d/56/921726b776ace8d8f5db44c4ef961006580d91dc52b803c489fafd1aa249/pydantic_core-2.46.4.tar.gz" + sha256 "62f875393d7f270851f20523dd2e29f082bcc82292d66db2b64ea71f64b6e1c1" + end + + resource "pydantic-settings" do + url "https://files.pythonhosted.org/packages/07/60/1d1e59c9c90d54591469ada7d268251f71c24bdb765f1a8a832cee8c6653/pydantic_settings-2.14.1.tar.gz" + sha256 "e874d3bec7e787b0c9958277956ed9b4dd5de6a80e162188fdaff7c5e26fd5fa" + end + + resource "pygments" do + url "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz" + sha256 "6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f" + end + + resource "pyjwt" do + url "https://files.pythonhosted.org/packages/c2/27/a3b6e5bf6ff856d2509292e95c8f57f0df7017cf5394921fc4e4ef40308a/pyjwt-2.12.1.tar.gz" + sha256 "c74a7a2adf861c04d002db713dd85f84beb242228e671280bf709d765b03672b" + end + + resource "pyperclip" do + url "https://files.pythonhosted.org/packages/e8/52/d87eba7cb129b81563019d1679026e7a112ef76855d6159d24754dbd2a51/pyperclip-1.11.0.tar.gz" + sha256 "244035963e4428530d9e3a6101a1ef97209c6825edab1567beac148ccc1db1b6" + end + + resource "python-dotenv" do + url "https://files.pythonhosted.org/packages/82/ed/0301aeeac3e5353ef3d94b6ec08bbcabd04a72018415dcb29e588514bba8/python_dotenv-1.2.2.tar.gz" + sha256 "2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3" + end + + resource "python-multipart" do + url "https://files.pythonhosted.org/packages/82/54/a85eb421fbdd5007bc5af39d0f4ed9fa609e0fedbfdc2adcf0b34526870e/python_multipart-0.0.28.tar.gz" + sha256 "8550da197eac0f7ab748961fc9509b999fa2662ea25cef857f05249f6893c0f8" + end + + resource "pyyaml" do + url "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz" + sha256 "d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f" + end + + resource "ratelimit" do + url "https://files.pythonhosted.org/packages/ab/38/ff60c8fc9e002d50d48822cc5095deb8ebbc5f91a6b8fdd9731c87a147c9/ratelimit-2.2.1.tar.gz" + sha256 "af8a9b64b821529aca09ebaf6d8d279100d766f19e90b5059ac6a718ca6dee42" + end + + resource "referencing" do + url "https://files.pythonhosted.org/packages/22/f5/df4e9027acead3ecc63e50fe1e36aca1523e1719559c499951bb4b53188f/referencing-0.37.0.tar.gz" + sha256 "44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8" + end + + resource "rich" do + url "https://files.pythonhosted.org/packages/c0/8f/0722ca900cc807c13a6a0c696dacf35430f72e0ec571c4275d2371fca3e9/rich-15.0.0.tar.gz" + sha256 "edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36" + end + + resource "rich-rst" do + url "https://files.pythonhosted.org/packages/bc/6d/a506aaa4a9eaa945ed8ab2b7347859f53593864289853c5d6d62b77246e0/rich_rst-1.3.2.tar.gz" + sha256 "a1196fdddf1e364b02ec68a05e8ff8f6914fee10fbca2e6b6735f166bb0da8d4" + end + + resource "rpds-py" do + url "https://files.pythonhosted.org/packages/20/af/3f2f423103f1113b36230496629986e0ef7e199d2aa8392452b484b38ced/rpds_py-0.30.0.tar.gz" + sha256 "dd8ff7cf90014af0c0f787eea34794ebf6415242ee1d6fa91eaba725cc441e84" + end + + resource "secretstorage" do + url "https://files.pythonhosted.org/packages/1c/03/e834bcd866f2f8a49a85eaff47340affa3bfa391ee9912a952a1faa68c7b/secretstorage-3.5.0.tar.gz" + sha256 "f04b8e4689cbce351744d5537bf6b1329c6fc68f91fa666f60a380edddcd11be" + end + + resource "shellingham" do + url "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz" + sha256 "8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de" + end + + resource "sse-starlette" do + url "https://files.pythonhosted.org/packages/f7/2b/58abc2d1fd397e7dde08e947e05c884d8ef2f78d5e2588c17a12d42d6994/sse_starlette-3.4.4.tar.gz" + sha256 "07e0fa0460138baf25cdd5fb28683472c3995dc1642225191b3832d62526bcb0" + end + + resource "starlette" do + url "https://files.pythonhosted.org/packages/81/69/17425771797c36cded50b7fe44e850315d039f28b15901ab44839e70b593/starlette-1.0.0.tar.gz" + sha256 "6a4beaf1f81bb472fd19ea9b918b50dc3a77a6f2e190a12954b25e6ed5eea149" + end + + resource "tenacity" do + url "https://files.pythonhosted.org/packages/47/c6/ee486fd809e357697ee8a44d3d69222b344920433d3b6666ccd9b374630c/tenacity-9.1.4.tar.gz" + sha256 "adb31d4c263f2bd041081ab33b498309a57c77f9acf2db65aadf0898179cf93a" + end + + resource "typer" do + url "https://files.pythonhosted.org/packages/e4/51/9aed62104cea109b820bbd6c14245af756112017d309da813ef107d42e7e/typer-0.25.1.tar.gz" + sha256 "9616eb8853a09ffeabab1698952f33c6f29ffdbceb4eaeecf571880e8d7664cc" + end + + resource "typing-extensions" do + url "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz" + sha256 "0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466" + end + + resource "typing-inspection" do + url "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz" + sha256 "ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464" + end + + resource "uncalled-for" do + url "https://files.pythonhosted.org/packages/b5/82/345cc927f7fbdae6065e7768759932fcc827fc20b29b45dfbafa2f1f7da4/uncalled_for-0.3.2.tar.gz" + sha256 "89f5dbcd71e2b8f47c030b1fa302e6cce2ec795d1ac565eeb6525c5fe55cb8a2" + end + + resource "uvicorn" do + url "https://files.pythonhosted.org/packages/f6/b1/8e7077a8641086aea449e1b5752a570f1b5906c64e0a33cd6d93b63a066b/uvicorn-0.47.0.tar.gz" + sha256 "7c9a0ea1a9414106bbab7324609c162d8fa0cdcdcb703060987269d77c7bb533" + end + + resource "watchfiles" do + url "https://files.pythonhosted.org/packages/c2/c9/8869df9b2a2d6c59d79220a4db37679e74f807c559ffe5265e08b227a210/watchfiles-1.1.1.tar.gz" + sha256 "a173cb5c16c4f40ab19cecf48a534c409f7ea983ab8fed0741304a1c0a31b3f2" + end + + resource "websockets" do + url "https://files.pythonhosted.org/packages/04/24/4b2031d72e840ce4c1ccb255f693b15c334757fc50023e4db9537080b8c4/websockets-16.0.tar.gz" + sha256 "5f6261a5e56e8d5c42a4497b364ea24d94d9563e8fbd44e78ac40879c60179b5" + end + + resource "zipp" do + url "https://files.pythonhosted.org/packages/30/21/093488dfc7cc8964ded15ab726fad40f25fd3d788fd741cc1c5a17d78ee8/zipp-3.23.1.tar.gz" + sha256 "32120e378d32cd9714ad503c1d024619063ec28aad2248dc6672ad13edfa5110" + end + + def install + virtualenv_install_with_resources + end + + test do + assert_match "Usage", shell_output("#{bin}/fli --help") + assert_match "flights", shell_output("#{bin}/fli --help") + assert_path_exists bin/"fli-mcp" + # Confirm the MCP optional deps actually landed in the venv so that + # ``fli-mcp`` doesn't ``ModuleNotFoundError`` on first run. + system libexec/"bin/python", "-c", "import fastmcp, fastapi" + end +end diff --git a/README.md b/README.md index 0c24a16f..d90f8ec8 100644 --- a/README.md +++ b/README.md @@ -113,6 +113,26 @@ pipx install flights fli --help ``` +### Homebrew (macOS / Linux) + +```bash +brew install punitarani/fli/flights +``` + +The formula name is `flights` (matching the PyPI package); the installed CLI +is `fli` (with `fli-mcp` and `fli-mcp-http` alongside it). The whole thing +runs in a self-contained virtualenv — no system Python required. + +To upgrade later: + +```bash +brew upgrade punitarani/fli/flights +``` + +> A single-token `brew install flights` would require landing the formula in +> `Homebrew/homebrew-core`. The formula in this repo is ready for that +> submission; until then, use the tap path above. + ![CLI Demo](https://github.com/punitarani/fli/blob/main/data/cli-demo.png) ## Features diff --git a/scripts/generate_homebrew_formula.py b/scripts/generate_homebrew_formula.py new file mode 100644 index 00000000..fe5e2127 --- /dev/null +++ b/scripts/generate_homebrew_formula.py @@ -0,0 +1,172 @@ +#!/usr/bin/env python3 +"""Regenerate ``Formula/flights.rb`` for a given ``flights`` PyPI release. + +This resolves the dependency closure of ``flights==`` against PyPI, +fetches each sdist's URL and SHA-256, and writes a Homebrew formula that uses +``Language::Python::Virtualenv``. Run it after each release to refresh the +tap so ``brew install punitarani/fli/flights`` pulls the latest version. + +Usage +----- + + uv run python scripts/generate_homebrew_formula.py [VERSION] + +If ``VERSION`` is omitted, the version from ``pyproject.toml`` is used. + +Requirements +------------ + +* ``uv`` on PATH (used to resolve the dependency closure). +* Network access to ``pypi.org``. +""" + +from __future__ import annotations + +import json +import re +import subprocess +import sys +import urllib.request +from pathlib import Path + +import tomllib + +ROOT = Path(__file__).resolve().parent.parent +FORMULA_PATH = ROOT / "Formula" / "flights.rb" +PYPROJECT = ROOT / "pyproject.toml" + +# Bump in lockstep with the `depends_on "python@..."` line in the template. +PYTHON_VERSION = "3.13" + + +def normalize(name: str) -> str: + """Return the PEP 503 normalized form of a distribution name.""" + return re.sub(r"[-_.]+", "-", name).lower() + + +def project_version() -> str: + """Return the ``flights`` version declared in ``pyproject.toml``.""" + return tomllib.loads(PYPROJECT.read_text())["project"]["version"] + + +def resolve_closure(version: str) -> list[tuple[str, str]]: + """Resolve ``flights==version`` and return ``[(name, version), ...]``.""" + out = subprocess.run( + [ + "uv", + "pip", + "compile", + "--quiet", + "--no-header", + "--no-annotate", + "--python-version", + PYTHON_VERSION, + "-", + ], + input=f"flights[all]=={version}\n", + check=True, + capture_output=True, + text=True, + ).stdout + + pkgs: list[tuple[str, str]] = [] + for line in out.splitlines(): + line = line.strip() + if not line or line.startswith("#") or line.startswith("-"): + continue + if "==" not in line: + continue + name, ver = line.split("==", 1) + ver = ver.split()[0].split(";")[0].strip() + pkgs.append((name.strip(), ver)) + + # `flights[all]` has ~80 transitive deps; anything dramatically smaller + # means we silently dropped most of the parser output and would ship a + # broken formula. + if len(pkgs) < 10: + raise RuntimeError( + f"Resolved closure has only {len(pkgs)} packages; expected >= 10. " + f"`uv pip compile` output may have changed format. Raw output:\n{out}" + ) + return pkgs + + +def fetch_sdist(name: str, version: str) -> tuple[str, str]: + """Return the ``(url, sha256)`` of the sdist for ``name==version`` on PyPI.""" + url = f"https://pypi.org/pypi/{name}/{version}/json" + req = urllib.request.Request(url, headers={"User-Agent": "fli-homebrew-gen"}) + with urllib.request.urlopen(req, timeout=30) as resp: + data = json.load(resp) + sdist = next((u for u in data["urls"] if u["packagetype"] == "sdist"), None) + if sdist is None: + raise RuntimeError(f"No sdist available for {name}=={version}") + return sdist["url"], sdist["digests"]["sha256"] + + +FORMULA_TEMPLATE = """\ +class Flights < Formula + include Language::Python::Virtualenv + + desc "Programmatic access to Google Flights via the fli CLI" + homepage "https://github.com/punitarani/fli" + url "{url}" + sha256 "{sha}" + license "MIT" + + livecheck do + url :stable + strategy :pypi + end + + depends_on "rust" => :build + depends_on "libyaml" + depends_on "python@{python_version}" + +{resources} + + def install + virtualenv_install_with_resources + end + + test do + assert_match "Usage", shell_output("#{{bin}}/fli --help") + assert_match "flights", shell_output("#{{bin}}/fli --help") + assert_path_exists bin/"fli-mcp" + # Confirm the MCP optional deps actually landed in the venv so that + # ``fli-mcp`` doesn't ``ModuleNotFoundError`` on first run. + system libexec/"bin/python", "-c", "import fastmcp, fastapi" + end +end +""" + + +def build_resource_block(name: str, version: str) -> str: + """Render a single Homebrew ``resource ... do ... end`` stanza.""" + url, sha = fetch_sdist(name, version) + return f' resource "{normalize(name)}" do\n url "{url}"\n sha256 "{sha}"\n end' + + +def main(argv: list[str]) -> int: + """Write ``Formula/flights.rb`` for the requested ``flights`` version.""" + version = argv[1] if len(argv) > 1 else project_version() + flights_url, flights_sha = fetch_sdist("flights", version) + + closure = resolve_closure(version) + deps = [(n, v) for n, v in closure if normalize(n) != "flights"] + deps.sort(key=lambda nv: normalize(nv[0])) + + resources = "\n\n".join(build_resource_block(n, v) for n, v in deps) + FORMULA_PATH.write_text( + FORMULA_TEMPLATE.format( + url=flights_url, + sha=flights_sha, + resources=resources, + python_version=PYTHON_VERSION, + ) + ) + print(f"Wrote {FORMULA_PATH.relative_to(ROOT)} for flights=={version}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main(sys.argv))