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/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..00ed18c 100644 --- a/mix.exs +++ b/mix.exs @@ -40,16 +40,20 @@ defmodule Location.MixProject do defp extra_applications(_env), do: [] # 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]} + {:nimble_csv, "~> 1.2"}, + {:floki, "~> 0.35.2"}, + {:tesla, "~> 1.8"}, + {:hackney, "~> 1.20"}, + {:flow, "~> 1.2"}, + {:unzip, "0.11.0"}, + {:csv, "~> 3.2"} ] end end diff --git a/mix.lock b/mix.lock index 9db9013..0c0c65e 100644 --- a/mix.lock +++ b/mix.lock @@ -1,7 +1,19 @@ %{ - "floki": {:hex, :floki, "0.36.2", "a7da0193538c93f937714a6704369711998a51a6164a222d710ebd54020aa7a3", [:mix], [], "hexpm", "a8766c0bc92f074e5cb36c4f9961982eda84c5d2b8e979ca67f5c268ec8ed580"}, + "certifi": {:hex, :certifi, "2.14.0", "ed3bef654e69cde5e6c022df8070a579a79e8ba2368a00acf3d75b82d9aceeed", [:rebar3], [], "hexpm", "ea59d87ef89da429b8e905264fdec3419f84f2215bb3d81e07a18aac919026c3"}, + "csv": {:hex, :csv, "3.2.2", "452f96414b39a176b7c390af6d8b78f15130dc6167fe3b836729131f515d843e", [:mix], [], "hexpm", "cbf256ff74a3fa01d9ec420d07b19c90d410ed9fe5b6d6e1bc7662edf35bc574"}, + "floki": {:hex, :floki, "0.35.4", "cc947b446024732c07274ac656600c5c4dc014caa1f8fb2dfff93d275b83890d", [:mix], [], "hexpm", "27fa185d3469bd8fc5947ef0f8d5c4e47f0af02eb6b070b63c868f69e3af0204"}, "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"}, + "hackney": {:hex, :hackney, "1.22.0", "4efc68df70322d4d2e3d2744e9bd191a39a0cb8d08c35379a08d9fb0f040d595", [:rebar3], [{:certifi, "~> 2.14.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~> 6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~> 1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~> 1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.4.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~> 1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "628569e451820950382be3d3e6481d7c59997e606c7823bddb4ce5d10812dfcb"}, + "idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"}, "jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"}, + "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"}, + "mime": {:hex, :mime, "2.0.6", "8f18486773d9b15f95f4f4f1e39b710045fa1de891fada4516559967276e4dc2", [:mix], [], "hexpm", "c9945363a6b26d747389aac3643f8e0e09d30499a138ad64fe8fd1d13d9b153e"}, + "mimerl": {:hex, :mimerl, "1.3.0", "d0cd9fc04b9061f82490f6581e0128379830e78535e017f7780f37fea7545726", [:rebar3], [], "hexpm", "a1e15a50d1887217de95f0b9b0793e32853f7c258a5cd227650889b38839fe9d"}, "nimble_csv": {:hex, :nimble_csv, "1.2.0", "4e26385d260c61eba9d4412c71cea34421f296d5353f914afe3f2e71cce97722", [:mix], [], "hexpm", "d0628117fcc2148178b034044c55359b26966c6eaa8e2ce15777be3bbc91b12a"}, + "parse_trans": {:hex, :parse_trans, "3.4.1", "6e6aa8167cb44cc8f39441d05193be6e6f4e7c2946cb2759f015f8c56b76e5ff", [:rebar3], [], "hexpm", "620a406ce75dada827b82e453c19cf06776be266f5a67cff34e1ef2cbb60e49a"}, + "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.7", "354c321cf377240c7b8716899e182ce4890c5938111a1296add3ec74cf1715df", [:make, :mix, :rebar3], [], "hexpm", "fe4c190e8f37401d30167c8c405eda19469f34577987c76dde613e838bbc67f8"}, + "tesla": {:hex, :tesla, "1.14.1", "71c5b031b4e089c0fbfb2b362e24b4478465773ae4ef569760a8c2899ad1e73c", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:exjsx, ">= 3.0.0", [hex: :exjsx, 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", [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]}, {:poison, ">= 1.0.0", [hex: :poison, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm", "c1dde8140a49a3bef5bb622356e77ac5a24ad0c8091f12c3b7fc1077ce797155"}, + "unicode_util_compat": {:hex, :unicode_util_compat, "0.7.0", "bc84380c9ab48177092f43ac89e4dfa2c6d62b40b8bd132b1059ecc7232f9a78", [:rebar3], [], "hexpm", "25eee6d67df61960cf6a794239566599b09e17e668d3700247bc498638152521"}, + "unzip": {:hex, :unzip, "0.11.0", "ffa85fede998a84c7d7eb026f99d2eb13a7806a121e9239503f310062185cdce", [:mix], [], "hexpm", "f536dc40011d4a0d6d3ddb0919daea01684912fa62b6b9495ded879a1fd8b265"}, } 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 deleted file mode 100644 index f69ed48..0000000 --- a/priv/version +++ /dev/null @@ -1 +0,0 @@ -2024-07-09 \ 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