diff --git a/README.md b/README.md index 8379d81..c3bcb74 100644 --- a/README.md +++ b/README.md @@ -15,3 +15,10 @@ The data for subdivisions comes primarily from the [debian iso-codes](https://sa The data for cities comes from the [geonames](http://www.geonames.org/) project. This project has scripts to download the main `allCountries.txt` file. It is then processed to make it smaller (from 1.3GB to about 130MB). Still, the resulting file is quite large so we also provide a city database based on the smaller `cities500.txt` file. + +### Postal Codes + +The data for postal codes comes from the [geonames](http://www.geonames.org/) project. This project has scripts to download all or individual postal code files via the --source option. + +### Mix Tasks +one must first refresh the applications task lists by running `mix compile` tasks can then be viewed via `mix help` \ No newline at end of file diff --git a/lib/location.ex b/lib/location.ex index 01370f6..a3fc997 100644 --- a/lib/location.ex +++ b/lib/location.ex @@ -1,3 +1,4 @@ +NimbleCSV.define(PostCodeCSV, separator: ",", escape: "\~") NimbleCSV.define(LocationCSV, separator: "\t", escape: "\~") defmodule Location do @@ -8,6 +9,16 @@ defmodule Location do defdelegate search_subdivision(code), to: Location.Subdivision defdelegate get_city(code), to: Location.City defdelegate get_city(city_name, country_code), to: Location.City + defdelegate get_postal_code(code), to: Location.PostalCode + defdelegate get_postal_codes(country_code, state_code, city_name), to: Location.PostalCode + defdelegate get_postal_codes(), to: Location.PostalCode + + def unload_all()do + :ok = unload(Location.Country) + :ok = unload(Location.Subdivision) + :ok = unload(Location.City) + :ok = unload(Location.PostalCode) + end def load_all() do Logger.debug("Loading location databases...") @@ -15,9 +26,10 @@ defmodule Location do :ok = load(Location.Country) :ok = load(Location.Subdivision) :ok = load(Location.City) + :ok = load(Location.PostalCode) end - defp load(module) do + def load(module) do {t, _result} = :timer.tc(fn -> module.load() @@ -29,6 +41,13 @@ defmodule Location do :ok end + def unload(module) do + module.unload() + + Logger.debug("Unloading location database #{inspect(module)}") + :ok + end + def version() do version_file = Application.app_dir(:location, "priv/version") diff --git a/lib/location/city.ex b/lib/location/city.ex index 4a5f275..adf39d0 100644 --- a/lib/location/city.ex +++ b/lib/location/city.ex @@ -4,6 +4,11 @@ defmodule Location.City do defstruct [:id, :name, :country_code] + def unload()do + :ets.delete(@ets_table_by_id) + :ets.delete(@ets_table_by_label) + end + def load() do @ets_table_by_id = :ets.new(@ets_table_by_id, [ diff --git a/lib/location/country.ex b/lib/location/country.ex index aa32730..ff24218 100644 --- a/lib/location/country.ex +++ b/lib/location/country.ex @@ -3,6 +3,10 @@ defmodule Location.Country do defstruct [:alpha_2, :alpha_3, :name, :flag] + def unload()do + :ets.delete(@ets_table) + end + def load() do ets = :ets.new(@ets_table, [:named_table]) diff --git a/mix_tasks/http.ex b/lib/location/http.ex similarity index 100% rename from mix_tasks/http.ex rename to lib/location/http.ex diff --git a/lib/location/postalcode.ex b/lib/location/postalcode.ex new file mode 100644 index 0000000..0b4c8c2 --- /dev/null +++ b/lib/location/postalcode.ex @@ -0,0 +1,236 @@ +defmodule Location.PostalCode do + @ets_table_by_id __MODULE__ + @ets_table_by_lookup Module.concat(__MODULE__, ByLookup) + + defstruct [ + :postal_code, + :country_code, + :state_code, + :city_name, + :latitude, + :longitude + ] + + def unload()do + :ets.delete(@ets_table_by_id) + :ets.delete(@ets_table_by_lookup) + end + + def load() do + @ets_table_by_lookup = + :ets.new(@ets_table_by_lookup, [ + :set, + :named_table, + :public, + :compressed, + {:write_concurrency, true}, + {:read_concurrency, true}, + {:decentralized_counters, false} + ]) + + @ets_table_by_id = + :ets.new(@ets_table_by_id, [ + :set, + :named_table, + :public, + :compressed, + {:write_concurrency, true}, + {:read_concurrency, true}, + {:decentralized_counters, false} + ]) + + source_file() + |> File.stream!() + |> Stream.chunk_every(15_000) + |> Task.async_stream( + fn chunk -> + chunk + |> PostCodeCSV.parse_stream() + |> Stream.each(fn data -> + __MODULE__.parse(data) + end) + |> Stream.run() + end, + timeout: :infinity + ) + |> Stream.run() + end + + def parse(data) do + case data do + [ + country_code, + postal_code, + city_name, + _state_name, + state_code, + _municipality, + _municipality_code, + _admin_name3, + _admin_code3, + latitude, + longitude, + _accuracy, + _, + _ + ] -> + country_code = String.trim(country_code) + + true = + :ets.insert( + @ets_table_by_lookup, + {{country_code, state_code, city_name}, {postal_code, latitude, longitude}} + ) + + true = + :ets.insert( + @ets_table_by_id, + {postal_code, {country_code, state_code, city_name, latitude, longitude}} + ) + + [ + country_code, + postal_code, + city_name, + _state_name, + state_code, + _municipality, + _municipality_code, + _admin_name3, + _admin_code3, + latitude, + longitude, + _accuracy, + _ + ] -> + country_code = String.trim(country_code) + + true = + :ets.insert( + @ets_table_by_lookup, + {{country_code, state_code, city_name}, {postal_code, latitude, longitude}} + ) + + true = + :ets.insert( + @ets_table_by_id, + {postal_code, {country_code, state_code, city_name, latitude, longitude}} + ) + + [ + country_code, + postal_code, + city_name, + _state_name, + state_code, + _municipality, + _municipality_code, + _admin_name3, + _admin_code3, + latitude, + longitude, + _accuracy + ] -> + country_code = String.trim(country_code) + + true = + :ets.insert( + @ets_table_by_lookup, + {{country_code, state_code, city_name}, {postal_code, latitude, longitude}} + ) + + true = + :ets.insert( + @ets_table_by_id, + {postal_code, {country_code, state_code, city_name, latitude, longitude}} + ) + + [ + country_code, + postal_code, + city_name, + _state_name, + state_code, + _municipality, + _municipality_code, + _admin_name3, + _admin_code3, + latitude, + longitude + ] -> + true = + :ets.insert( + @ets_table_by_lookup, + {{country_code, state_code, city_name}, {postal_code, latitude, longitude}} + ) + + true = + :ets.insert( + @ets_table_by_id, + {postal_code, {country_code, state_code, city_name, latitude, longitude}} + ) + + _data -> + :ok + end + end + + @doc """ + Finds postal_code information by postal code. + """ + @spec get_postal_code(String.t()) :: %__MODULE__{} | nil + def get_postal_code(code) do + case :ets.lookup(@ets_table_by_id, code) do + [{postal_code, {country_code, state_code, city_name, latitude, longitude}}] -> + to_struct(postal_code, country_code, state_code, city_name, latitude, longitude) + + _ -> + nil + end + end + + @doc """ + Finds postal codes by city code, state code and country code. + + This function returns all postal code founds when the country has multiple + cities with the same name. + """ + @spec get_postal_codes(String.t(), String.t(), String.t()) :: %__MODULE__{} | nil + def get_postal_codes(country_code, state_code, city_name) do + case :ets.lookup(@ets_table_by_lookup, {country_code, state_code, city_name}) do + data when is_list(data) -> + Enum.map(data, fn x -> + {{country_code, state_code, city_name}, {postal_code, latitude, longitude}} = x + to_struct(postal_code, country_code, state_code, city_name, latitude, longitude) + end) + + _ -> + nil + end + end + + @spec get_postal_codes() :: %__MODULE__{} | nil + def get_postal_codes() do + :ets.tab2list(@ets_table_by_lookup) + |> Enum.map(fn x -> + {{country_code, state_code, city_name}, {postal_code, latitude, longitude}} = x + to_struct(postal_code, country_code, state_code, city_name, latitude, longitude) + end) + end + + defp source_file() do + default = Application.app_dir(:location, "/priv/postal_codes.csv") + Application.get_env(:location, :postal_codes_source_file, default) + end + + defp to_struct(postal_code, country_code, state_code, city_name, latitude, longitude) do + %__MODULE__{ + postal_code: postal_code, + country_code: country_code, + state_code: state_code, + city_name: city_name, + latitude: latitude, + longitude: longitude + } + end +end diff --git a/lib/location/scraper.ex b/lib/location/scraper.ex new file mode 100644 index 0000000..a5db935 --- /dev/null +++ b/lib/location/scraper.ex @@ -0,0 +1,50 @@ +defmodule Location.Scraper do + use Tesla + + @version_file Application.app_dir(:location, "/priv/version") + @postal_code_url "https://download.geonames.org/export/zip/" + @postal_code_dest Application.app_dir(:location, "/priv/") + + def write_date_to_version() do + File.write!(@version_file, Date.to_iso8601(Date.utc_today())) + end + + def scrape_postal_files() do + response = get!(@postal_code_url) + {:ok, document} = Floki.parse_document(response.body) + + result = + Floki.find(document, "pre") + + result = Floki.find(result, "a") |> Enum.drop(5) + + Enum.map(result, fn x -> + [{_, [{_, _href}], [name]}] = Floki.find(x, "a") + String.replace(name, ".zip", "") + end) + |> Enum.join(", ") + end + + def fetch_postal_file(file) do + response = get!(@postal_code_url <> "#{file}.zip") + File.write!(@postal_code_dest <> "/#{file}.zip", response.body) + end + + def extract_postal_file(file) do + zip_file = Unzip.LocalFile.open("priv/#{file}.zip") + + try do + {:ok, unzip} = Unzip.new(zip_file) + + Unzip.file_stream!(unzip, "#{file}.txt") + |> Stream.into(File.stream!("priv/#{file}.csv")) + |> Stream.run() + after + Unzip.LocalFile.close(zip_file) + end + end + + def fetch_postal_files(files) do + Enum.each(files, fn file -> fetch_postal_file(file) end) + end +end diff --git a/lib/location/subdivision.ex b/lib/location/subdivision.ex index ce4ace8..ab8dabb 100644 --- a/lib/location/subdivision.ex +++ b/lib/location/subdivision.ex @@ -3,6 +3,10 @@ defmodule Location.Subdivision do defstruct [:code, :name, :type, :country_code] + def unload()do + :ets.delete(@ets_table) + end + def load() do ets = :ets.new(@ets_table, [:named_table]) diff --git a/mix_tasks/update_english_translations.ex b/lib/mix/tasks/update_english_translations.ex similarity index 98% rename from mix_tasks/update_english_translations.ex rename to lib/mix/tasks/update_english_translations.ex index 5f717b5..03cb873 100644 --- a/mix_tasks/update_english_translations.ex +++ b/lib/mix/tasks/update_english_translations.ex @@ -1,5 +1,6 @@ -defmodule Mix.Tasks.UpdateEnglishTranslations do +defmodule Mix.Tasks.Location.UpdateEnglishTranslations do use Mix.Task + @shortdoc "Updates english translations for locations" @cldr_url "https://raw.githubusercontent.com/unicode-org/cldr/main/common/subdivisions/en.xml" @translations_dest Application.app_dir(:location, "/priv/iso_3166-2.en-translations.json") diff --git a/mix_tasks/update_geoname_data.ex b/lib/mix/tasks/update_geoname_data.ex similarity index 70% rename from mix_tasks/update_geoname_data.ex rename to lib/mix/tasks/update_geoname_data.ex index 51a4159..9a20fbb 100644 --- a/mix_tasks/update_geoname_data.ex +++ b/lib/mix/tasks/update_geoname_data.ex @@ -1,15 +1,20 @@ -defmodule Mix.Tasks.UpdateGeonameData do +defmodule Mix.Tasks.Location.UpdateGeonameData do use Mix.Task - - # @allcountries_src "https://download.geonames.org/export/dump/allCountries.zip" + @shortdoc "Updates the geonamedata for locations" + @allcountries_src "https://download.geonames.org/export/dump/allCountries.zip" @allcountries_dest Application.app_dir(:location, "/priv/geonames.csv") @doc """ The data source allCountries.txt clocks in at 1.5GB. Expect this to take a while. """ def run(_) do - # System.cmd("wget", [@allcountries_src, "-O", "/tmp/allCountries.zip"]) - # System.cmd("unzip", ["/tmp/allCountries.zip", "-d", "/tmp"]) + System.cmd("wget", [@allcountries_src, "-O", "/tmp/allCountries.zip"]) + zip_file = Unzip.LocalFile.open("/tmp/allCountries.zip") + {:ok, unzip} = Unzip.new(zip_file) + + Unzip.file_stream!(unzip, "allCountries.txt") + |> Stream.into(File.stream!("/tmp/allCountries.txt")) + |> Stream.run() process_geonames_file("/tmp/allCountries.txt") end diff --git a/mix_tasks/update_iso_data.ex b/lib/mix/tasks/update_iso_data.ex similarity index 95% rename from mix_tasks/update_iso_data.ex rename to lib/mix/tasks/update_iso_data.ex index a09a710..4ae23da 100644 --- a/mix_tasks/update_iso_data.ex +++ b/lib/mix/tasks/update_iso_data.ex @@ -1,5 +1,6 @@ -defmodule Mix.Tasks.UpdateIsoData do +defmodule Mix.Tasks.Location.UpdateIsoData do use Mix.Task + @shortdoc "Updates the isocodes for locations" @countries_src "https://salsa.debian.org/iso-codes-team/iso-codes/-/raw/main/data/iso_3166-1.json" @subdivisions_src "https://salsa.debian.org/iso-codes-team/iso-codes/-/raw/main/data/iso_3166-2.json" diff --git a/lib/mix/tasks/update_postal_code_data.ex b/lib/mix/tasks/update_postal_code_data.ex new file mode 100644 index 0000000..bc3cb64 --- /dev/null +++ b/lib/mix/tasks/update_postal_code_data.ex @@ -0,0 +1,80 @@ +defmodule Mix.Tasks.Location.UpdatePostalCodeData do + use Mix.Task + @shortdoc "Updates the postal code data from source" + + @destination_filename Application.compile_env( + :location, + :postal_codes_source_file, + "priv/postal_codes.csv" + ) + + @doc """ + The data source clocks in at 16mb. Expect this to take a while. + The option --source will download and parse different datasets ie. AZ (https://download.geonames.org/export/zip/AZ.zip) in order to keep the set small + """ + + def run(args) do + {options, _, _} = + OptionParser.parse(args, + strict: [source: :string, list: :boolean, append: :boolean, help: :boolean] + ) + + options = Keyword.merge([help: false, list: false, append: false], options) + + case(Keyword.get(options, :help) || Keyword.get(options, :list)) do + false -> + Keyword.get(options, :source) + |> main(Keyword.get(options, :append)) + + true -> + if(Keyword.get(options, :help)) do + IO.puts( + "The following options are available, --source 'Choose an option from --list', --list 'List of available countries by code', --append 'Append to the downloaded file (if you want multiple countries but not all)'" + ) + end + + if(Keyword.get(options, :list)) do + sources = Location.Scraper.scrape_postal_files() + IO.puts("The following Postal Code Sources are Available #{sources}") + end + end + end + + @doc """ + Fetch and Prepare a Postal Code Export + + """ + def main(name, append \\ false) do + src = "https://download.geonames.org/export/zip/#{name}.zip" + System.cmd("wget", [src, "-O", "/tmp/#{name}.zip"]) + + zip_file = Unzip.LocalFile.open("/tmp/#{name}.zip") + {:ok, unzip} = Unzip.new(zip_file) + + Unzip.file_stream!(unzip, "#{name}.txt") + |> Stream.into(File.stream!("/tmp/#{name}.txt")) + |> Stream.run() + + process_file("/tmp/#{name}.txt", append) + end + + defp process_file(filename, append) do + # BINARY + tab = :binary.compile_pattern("\t") + + result = + filename + |> File.stream!(read_ahead: 100_000) + |> Flow.from_enumerable() + |> Flow.map(&(String.trim(&1) |> String.split(tab))) + |> Flow.partition() + |> Enum.into([]) + + IO.puts("Writing result to #{@destination_filename}") + + Location.Scraper.write_date_to_version() + + file = File.open!(@destination_filename, [:write, :utf8]) + result |> CSV.encode() |> Enum.each(&IO.write(file, &1)) + end +end diff --git a/lib/mix/tasks/update_usps_data.ex b/lib/mix/tasks/update_usps_data.ex new file mode 100644 index 0000000..4a80e9f --- /dev/null +++ b/lib/mix/tasks/update_usps_data.ex @@ -0,0 +1,69 @@ +defmodule Mix.Tasks.Location.UpdateUspsData do + use Mix.Task + @shortdoc "Updates the postal code data from source" + + @destination_filename Application.compile_env( + :location, + :postal_codes_source_file, + "priv/postal_codes.csv" + ) + + @doc """ + The data source clocks in at 16mb. Expect this to take a while. + The option --source will download and parse different datasets ie. AZ (https://download.geonames.org/export/zip/AZ.zip) in order to keep the set small + """ + + def run(args) do + {options, _, _} = + OptionParser.parse(args, + strict: [append: :boolean] + ) + + options = Keyword.merge([help: false, list: false, append: false], options) + + main(Keyword.get(options, :append)) + end + + @doc """ + Fetch and Prepare a Postal Code Export + + """ + def main(append \\ false) do + ## fetch the file + {200, _, doc} = Location.HTTP.get!("https://postalpro.usps.com/ZIP_Locale_Detail") + {:ok, html} = Floki.parse_document(doc) + + [ + {"a", + [ + {"href", href}, + {"class", _}, + {"title", "Download ZIP Codes by Area and District codes"} + ], ["Download"]} + ] = Floki.find(html, ".mt-1") + + src = "https://postalpro.usps.com#{href}" + System.cmd("wget", [src, "-O", "/tmp/usps_zip_codes.xls"]) + process_file("/tmp/usps_zip_codes.xls", append) + end + + defp process_file(filename, append) do + # BINARY + tab = :binary.compile_pattern("\t") + + result = + filename + |> File.stream!(read_ahead: 100_000) + |> Flow.from_enumerable() + |> Flow.map(&(String.trim(&1) |> String.split(tab))) + |> Flow.partition() + |> Enum.into([]) + + IO.puts("Writing result to #{@destination_filename}") + + Location.Scraper.write_date_to_version() + + file = File.open!(@destination_filename, [:write, :utf8]) + result |> CSV.encode() |> Enum.each(&IO.write(file, &1)) + end +end diff --git a/location.iml b/location.iml new file mode 100644 index 0000000..89e3c09 --- /dev/null +++ b/location.iml @@ -0,0 +1,82 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/mix.exs b/mix.exs index ca83596..0201270 100644 --- a/mix.exs +++ b/mix.exs @@ -36,20 +36,25 @@ defmodule Location.MixProject do ] end - defp extra_applications(env) when env in [:dev, :test], do: [:inets, :ssl, :xmerl] - defp extra_applications(_env), do: [] + defp extra_applications(env) when env in [:dev, :test], do: [:inets, :ssl, :xmerl,:xlsxir] + defp extra_applications(_env), do: [:xlsxir] # Specifies which paths to compile per environment. - defp elixirc_paths(env) when env in [:dev, :test], do: ["lib", "mix_tasks"] + defp elixirc_paths(:test), do: ["lib"] defp elixirc_paths(_env), do: ["lib"] # Run "mix help deps" to learn about dependencies. defp deps do [ - {:jason, "~> 1.3"}, - {:nimble_csv, "~> 1.1"}, - {:floki, "~> 0.36.0", only: [:dev, :test]}, - {:flow, "~> 1.0", only: [:dev, :test]} + {:jason, ">= 0.0.0"}, + {:nimble_csv, ">= 0.0.0"}, + {:floki, ">= 0.0.0"}, + {:tesla, ">= 0.0.0"}, + {:hackney, ">= 0.0.0"}, + {:flow, ">= 0.0.0"}, + {:unzip, ">= 0.0.0"}, + {:csv, ">= 0.0.0"}, + {:xlsxir, ">= 0.0.0"}, ] end end diff --git a/mix.lock b/mix.lock index 9db9013..286667f 100644 --- a/mix.lock +++ b/mix.lock @@ -1,7 +1,24 @@ %{ - "floki": {:hex, :floki, "0.36.2", "a7da0193538c93f937714a6704369711998a51a6164a222d710ebd54020aa7a3", [:mix], [], "hexpm", "a8766c0bc92f074e5cb36c4f9961982eda84c5d2b8e979ca67f5c268ec8ed580"}, + "certifi": {:hex, :certifi, "2.17.0", "835748414307e15e05b17d0e518190228ce648b08d569a5cc93a85a40f3e5c9b", [:rebar3], [], "hexpm", "8122798a17f0293c80daada25d0f81c7f4d708c73fef782c7c9b1950e26e4d21"}, + "csv": {:hex, :csv, "3.2.2", "452f96414b39a176b7c390af6d8b78f15130dc6167fe3b836729131f515d843e", [:mix], [], "hexpm", "cbf256ff74a3fa01d9ec420d07b19c90d410ed9fe5b6d6e1bc7662edf35bc574"}, + "erlsom": {:hex, :erlsom, "1.5.2", "3e47c53a199136fb4d20d5479edb7c9229f31624534c062633951c8d14dcd276", [:rebar3], [], "hexpm", "4e765cc677fb30509f7b628ff2914e124cf4dcc0fac1c0a62ee4dcee24215b5d"}, + "floki": {:hex, :floki, "0.38.4", "10f98971e892aed2c2f1b3a0f928e488e3797e1c6dd3dfd98db40b14e9a78bcf", [:mix], [], "hexpm", "bdb34645eee8e79845c7edaca2d4099a52804ee4d4a3ecc683a69451f0244973"}, "flow": {:hex, :flow, "1.2.4", "1dd58918287eb286656008777cb32714b5123d3855956f29aa141ebae456922d", [:mix], [{:gen_stage, "~> 1.0", [hex: :gen_stage, repo: "hexpm", optional: false]}], "hexpm", "874adde96368e71870f3510b91e35bc31652291858c86c0e75359cbdd35eb211"}, - "gen_stage": {:hex, :gen_stage, "1.2.1", "19d8b5e9a5996d813b8245338a28246307fd8b9c99d1237de199d21efc4c76a1", [:mix], [], "hexpm", "83e8be657fa05b992ffa6ac1e3af6d57aa50aace8f691fcf696ff02f8335b001"}, - "jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"}, - "nimble_csv": {:hex, :nimble_csv, "1.2.0", "4e26385d260c61eba9d4412c71cea34421f296d5353f914afe3f2e71cce97722", [:mix], [], "hexpm", "d0628117fcc2148178b034044c55359b26966c6eaa8e2ce15777be3bbc91b12a"}, + "gen_stage": {:hex, :gen_stage, "1.3.2", "7c77e5d1e97de2c6c2f78f306f463bca64bf2f4c3cdd606affc0100b89743b7b", [:mix], [], "hexpm", "0ffae547fa777b3ed889a6b9e1e64566217413d018cabd825f786e843ffe63e7"}, + "h2": {:hex, :h2, "0.12.0", "f393539ee2728f8118fb2024b6d5f3e2c45e40ceb31b18b4e9bf5e50d028f80f", [:rebar3], [], "hexpm", "beaafc93c54cdc5d623247334d3970cdf4bc66b6b8b296b74ba1d7c7513c3dfc"}, + "hackney": {:hex, :hackney, "4.7.4", "8fe2ddaa3ca27de99d68e682d72b66d07d2331da680f77c8000580a0122c69e6", [:rebar3], [{:certifi, "~> 2.17.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:h2, "~> 0.12.0", [hex: :h2, repo: "hexpm", optional: false]}, {:idna, "~> 7.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:mimerl, "~> 1.5", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.4.2", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~> 1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:webtransport, "~> 0.4.5", [hex: :webtransport, repo: "hexpm", optional: false]}], "hexpm", "d07d7e1358353ab6cc75132f058c155287f3e013d43f709fbb79d79eeab98195"}, + "idna": {:hex, :idna, "7.1.0", "1067a13043538129602d2f2ce6899d8713125c7d19734aa557ce2e3ea55bd4f1", [:rebar3], [], "hexpm", "6ae959a025bf36df61a8cab8508d9654891b5426a84c44d82deaffd6ddf8c71f"}, + "jason": {:hex, :jason, "1.4.5", "2e3a008590b0b8d7388c20293e9dcc9cf3e5d642fd2a114e4cbbb52e595d940a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0 or ~> 3.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "b0c823996102bcd0239b3c2444eb00409b72f6a140c1950bc8b457d836b30684"}, + "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"}, + "mime": {:hex, :mime, "2.0.7", "b8d739037be7cd402aee1ba0306edfdef982687ee7e9859bee6198c1e7e2f128", [:mix], [], "hexpm", "6171188e399ee16023ffc5b76ce445eb6d9672e2e241d2df6050f3c771e80ccd"}, + "mimerl": {:hex, :mimerl, "1.5.0", "f35aca6f23242339b3666e0ac0702379e362b469d0aea167f6cc713547e777ed", [:rebar3], [], "hexpm", "db648ce065bae14ea84ca8b5dd123f42f49417cef693541110bf6f9e9be9ecc4"}, + "nimble_csv": {:hex, :nimble_csv, "1.3.0", "b7f998dc62b222bce9596e46f028c7a5af04cb5dde6df2ea197c583227c54971", [:mix], [], "hexpm", "41ccdc18f7c8f8bb06e84164fc51635321e80d5a3b450761c4997d620925d619"}, + "parse_trans": {:hex, :parse_trans, "3.4.2", "c352ddc1a0d5e54f9b1654d45f9c432eef76f9cea371c55ddff769ef688fdb74", [:rebar3], [], "hexpm", "4c25347de3b7c35732d32e69ab43d1ceee0beae3f3b3ade1b59cbd3dd224d9ca"}, + "quic": {:hex, :quic, "1.8.0", "7b074fc6f8a13b4a81e4e11254bf061db61c5c07b4366d568a79a38c5e8bb5f3", [:rebar3], [], "hexpm", "6b1f8f08a45412ac503bc6771efb8edc696bb14079bc48d2ca960b92933da8a4"}, + "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.7", "354c321cf377240c7b8716899e182ce4890c5938111a1296add3ec74cf1715df", [:make, :mix, :rebar3], [], "hexpm", "fe4c190e8f37401d30167c8c405eda19469f34577987c76dde613e838bbc67f8"}, + "tesla": {:hex, :tesla, "1.21.0", "de3dc7b0ddbbd72a2fdab02decc977c0fa910aa04cbfb92aaca5e93a839ad2a5", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:finch, "~> 0.13", [hex: :finch, repo: "hexpm", optional: true]}, {:fuse, "~> 2.4", [hex: :fuse, repo: "hexpm", optional: true]}, {:gun, ">= 1.0.0", [hex: :gun, repo: "hexpm", optional: true]}, {:hackney, "~> 1.21 or >= 4.0.2 and < 5.0.0-0", [hex: :hackney, repo: "hexpm", optional: true]}, {:ibrowse, "4.4.2", [hex: :ibrowse, repo: "hexpm", optional: true]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: true]}, {:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.0", [hex: :mint, repo: "hexpm", optional: true]}, {:mox, "~> 1.0", [hex: :mox, repo: "hexpm", optional: true]}, {:msgpax, "~> 2.3", [hex: :msgpax, repo: "hexpm", optional: true]}, {:opentelemetry_semantic_conventions, "~> 1.27", [hex: :opentelemetry_semantic_conventions, repo: "hexpm", optional: true]}, {:poison, ">= 1.0.0", [hex: :poison, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm", "1a0b8c9a7c3676beafd33db5e13f5e486af936c1a9a9f56b037e7861d916d333"}, + "unicode_util_compat": {:hex, :unicode_util_compat, "0.7.0", "bc84380c9ab48177092f43ac89e4dfa2c6d62b40b8bd132b1059ecc7232f9a78", [:rebar3], [], "hexpm", "25eee6d67df61960cf6a794239566599b09e17e668d3700247bc498638152521"}, + "unzip": {:hex, :unzip, "0.13.0", "bf5ec6ac6063c69e6ec54c8b4a3b8dcd7a2719d28d10d7025776ab107957cde9", [:mix], [], "hexpm", "4bcb9892ecbf2042606b43ab685a1bffe03c14003e6246f5453db2c829237fd9"}, + "webtransport": {:hex, :webtransport, "0.4.5", "0e387202bbe707389fe81373ef8c56faa9d5aa321bb4800fa4765ee7c1399785", [:rebar3], [{:h2, "~> 0.12", [hex: :h2, repo: "hexpm", optional: false]}, {:quic, "~> 1.8.0", [hex: :quic, repo: "hexpm", optional: false]}], "hexpm", "bcb512239e48e551d5bd5c667312a9a7de4f29b89d84b1d33c5af44e1f3f730d"}, + "xlsxir": {:hex, :xlsxir, "1.6.4", "d1e69439cbd9edc1190950f9f883ac364e1f31641e0395ccdb27761791b169a3", [:mix], [{:erlsom, "~> 1.5", [hex: :erlsom, repo: "hexpm", optional: false]}], "hexpm", "38e91f65eb8a4c8dea07d941c8b7e21baf8c8d4938232395c9ffd19d2eb071f2"}, } diff --git a/mix_tasks/scraper.ex b/mix_tasks/scraper.ex deleted file mode 100644 index e488b1d..0000000 --- a/mix_tasks/scraper.ex +++ /dev/null @@ -1,7 +0,0 @@ -defmodule Location.Scraper do - @version_file Application.app_dir(:location, "/priv/version") - - def write_date_to_version() do - File.write!(@version_file, Date.to_iso8601(Date.utc_today())) - end -end diff --git a/priv/version b/priv/version index f69ed48..85e029e 100644 --- a/priv/version +++ b/priv/version @@ -1 +1 @@ -2024-07-09 \ No newline at end of file +2025-02-26 \ No newline at end of file diff --git a/test/location_test.exs b/test/location_test.exs index edbb4d7..aea932a 100644 --- a/test/location_test.exs +++ b/test/location_test.exs @@ -216,4 +216,12 @@ defmodule LocationTest do assert city == %Location.City{country_code: "EE", name: "Tartu", id: 588_335} end end + + describe "postal code" do + test "can look up postal codes for a city" do + codes = Location.get_postal_codes("US", "AZ", "Tucson") + + assert Enum.count(codes) == 0 + end + end end