Skip to content

Commit

Permalink
Merge pull request #18 from Kociamber/update_versions_and_code
Browse files Browse the repository at this point in the history
Update to Elixir 1.17, update deps, tests and major refactor
  • Loading branch information
Kociamber authored Jul 11, 2024
2 parents 2eae18c + beb81d9 commit 59c89db
Show file tree
Hide file tree
Showing 26 changed files with 606 additions and 607 deletions.
3 changes: 2 additions & 1 deletion .tool-versions
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
elixir 1.12.1
elixir 1.17.1-otp-27
erlang 27.0
40 changes: 24 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,30 +1,33 @@
# ExOwm

[![Build Status](https://travis-ci.org/Kociamber/ex_owm.svg?branch=master)](https://travis-ci.org/Kociamber/ex_owm)
[![Hex version badge](https://img.shields.io/hexpm/v/ex_owm.svg)](https://hex.pm/packages/ex_owm)
[![Module Version](https://img.shields.io/hexpm/v/ex_owm.svg)](https://hex.pm/packages/ex_owm)
[![Hex Version](https://img.shields.io/hexpm/v/ex_owm.svg)](https://hex.pm/packages/ex_owm)
[![Hex Docs](https://img.shields.io/badge/docs-hexpm-blue.svg)](https://hexdocs.pm/ex_owm/)
[![License](https://img.shields.io/hexpm/l/ex_owm.svg)](https://github.com/kociamber/ex_owm/blob/master/LICENSE)
[![Total Download](https://img.shields.io/hexpm/dt/ex_owm.svg)](https://hex.pm/packages/ex_owm)
[![Last Updated](https://img.shields.io/github/last-commit/kociamber/ex_owm.svg)](https://github.com/kociamber/ex_owm/commits/master)

**Fast, industrial strength [Open Weather Map](http://openweathermap.org/technology) interface for Elixir platforms.**
**Fast [Open Weather Map](http://openweathermap.org/technology) API client for Elixir applications.**

## Installation

Add ExOwm as a dependency to your `mix.exs` file:

```elixir
defp deps() do
[{:ex_owm, "~> 1.2.3"}]
[{:ex_owm, "~> 1.3.0"}]
end
```

## Upgrade from 1.0.X

**Please re-factor** your configuration and paste below one once again as module naming (specifically the order) has slightly changed!
**Please re-factor** your configuration as the module naming (specifically the order) has slightly changed. Use the configuration below:

## Configuration

In order to be able to use OWM APIs, you need to [register](https://home.openweathermap.org/users/sign_up) free account and get free API KEY.
After obtaining the key, please set environmental variable called OWM_API_KEY and set the value to your API KEY.
To use OWM APIs, you need to [register](https://home.openweathermap.org/users/sign_up) for an account (free plan is available) and obtain an API key. After obtaining the key, set the environment variable OWM_API_KEY to your API key.

If you are going to use this application as a dependency in your own project, you will need to copy and paste below configuration to your `config/config.exs` file:
If you are using this application as a dependency in your project, add the following configuration to your `config/config.exs` file:

```elixir
config :ex_owm, api_key: System.get_env("OWM_API_KEY")
Expand All @@ -34,19 +37,20 @@ config :ex_owm, api_key: System.get_env("OWM_API_KEY")

## Basic Usage

ExOwm is currently handling the following main OpenWeatherMap [APIs](http://openweathermap.org/api):
ExOwm currently handles the following main OpenWeatherMap [APIs](http://openweathermap.org/api):

* [Current weather data](http://openweathermap.org/current)
* [One Call API](https://openweathermap.org/api/one-call-api)
* [One Call API History](https://openweathermap.org/api/one-call-api#history)
* [5 day / 3 hour forecast](http://openweathermap.org/forecast5)
* [1 - 16 day / daily forecast](http://openweathermap.org/forecast16)

Please note that with standard (free) license / API key you may be limited with amount of requests per minute and may not be able to access 1 - 16 day / daily forecast. Please refer to OpenWeatherMap license [plans](http://openweathermap.org/price).
Please note that with a standard (free) license/API key, you may be limited in the number of requests per minute and may not have access to the 1 - 16 day/daily forecast. Please refer to OpenWeatherMap [pricing plans](http://openweathermap.org/price) for more details..

There are three main public interface functions for each API and they accepts the same set of two params - a list of location maps and a keyword list of options.
There are three main public interface functions for each API, accepting the same set of two parameters: a list of location maps and a keyword list of options.

Sample API calls:

Sample API calls may look following:
```elixir
ExOwm.get_current_weather([%{city: "Warsaw"}, %{city: "London", country_code: "uk"}], units: :metric, lang: :pl)
[{:ok, %{WARSAW_DATA}}, {:ok, %{LONDON_DATA}}]
Expand All @@ -63,21 +67,25 @@ ExOwm.get_historical_weather([%{lat: 52.374031, lon: 4.88969, dt: yesterday}])

```

Please refer to official [docs](https://hexdocs.pm/ex_owm/readme.html) for more details.
For more details, refer to the official [docs](https://hexdocs.pm/ex_owm/readme.html).

## Overview

ExOwm is using cool features like:
ExOwm utilizes some cool features such as:

* concurrent API calls
* super fast generational caching
* access to **main** [OWM APIs](http://openweathermap.org/api)!

It means that each location entry passed within the list spawns separate task (Elixir worker process) which is checking whether the request has been already sent within a time interval, if yes, it's fetching the result from cache. Otherwise it sends API query, saves the result in cache and returns the data.
Each location entry in the list spawns a separate task (Elixir worker process) to check whether the request has been made within a specified time interval. If it has, the result is fetched from the cache. Otherwise, an API query is sent, the result is cached, and the data is returned.

## Running local tests

Since all the tests are based on OWM API calls, they are disabled by default. To enable them, please remove `:api_based_test` from the `test/test_helper.exs file`.

## To do

* Add Historical Data API
* Add remaining OWM APIs (including One Call API 3.0)

## License

Expand Down
2 changes: 1 addition & 1 deletion config/config.exs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use Mix.Config
import Config
config :logger, level: :info
config :ex_owm, api_key: System.get_env("OWM_API_KEY")

Expand Down
1 change: 1 addition & 0 deletions lib/application.ex
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
defmodule ExOwm.Application do
@moduledoc false
require Logger
use Application

Expand Down
21 changes: 15 additions & 6 deletions lib/ex_owm.ex
Original file line number Diff line number Diff line change
@@ -1,4 +1,13 @@
defmodule ExOwm do
alias ExOwm.{
Weather,
CurrentWeather,
FiveDayForecast,
HourlyForecast,
SixteenDayForecast,
HistoricalWeather
}

require Logger

@moduledoc """
Expand Down Expand Up @@ -47,7 +56,7 @@ defmodule ExOwm do
def get_current_weather(loc, opts \\ [])

def get_current_weather(locations, opts) when is_list(locations),
do: ExOwm.CurrentWeather.Coordinator.get_weather(locations, opts)
do: CurrentWeather.Coordinator.get_weather(locations, opts)

def get_current_weather(location, opts) when is_map(location),
do: get_current_weather([location], opts)
Expand All @@ -64,7 +73,7 @@ defmodule ExOwm do
def get_weather(loc, opts \\ [])

def get_weather(locations, opts) when is_list(locations),
do: ExOwm.Weather.Coordinator.get_weather(locations, opts)
do: Weather.Coordinator.get_weather(locations, opts)

def get_weather(location, opts) when is_map(location),
do: get_weather([location], opts)
Expand All @@ -81,7 +90,7 @@ defmodule ExOwm do
def get_five_day_forecast(locations, opts \\ [])

def get_five_day_forecast(locations, opts) when is_list(locations),
do: ExOwm.FiveDayForecast.Coordinator.get_weather(locations, opts)
do: FiveDayForecast.Coordinator.get_weather(locations, opts)

def get_five_day_forecast(location, opts) when is_map(location),
do: get_five_day_forecast([location], opts)
Expand All @@ -97,7 +106,7 @@ defmodule ExOwm do
def get_hourly_forecast(locations, opts \\ [])

def get_hourly_forecast(locations, opts) when is_list(locations),
do: ExOwm.HourlyForecast.Coordinator.get_weather(locations, opts)
do: HourlyForecast.Coordinator.get_weather(locations, opts)

def get_hourly_forecast(location, opts) when is_map(location),
do: get_hourly_forecast([location], opts)
Expand All @@ -114,7 +123,7 @@ defmodule ExOwm do
def get_sixteen_day_forecast(locations, opts \\ [])

def get_sixteen_day_forecast(locations, opts) when is_list(locations),
do: ExOwm.SixteenDayForecast.Coordinator.get_weather(locations, opts)
do: SixteenDayForecast.Coordinator.get_weather(locations, opts)

def get_sixteen_day_forecast(location, opts) when is_map(location),
do: get_sixteen_day_forecast([location], opts)
Expand All @@ -132,7 +141,7 @@ defmodule ExOwm do
def get_historical_weather(loc, opts \\ [])

def get_historical_weather(locations, opts) when is_list(locations),
do: ExOwm.HistoricalWeather.Coordinator.get_weather(locations, opts)
do: HistoricalWeather.Coordinator.get_weather(locations, opts)

def get_historical_weather(location, opts) when is_map(location),
do: get_historical_weather([location], opts)
Expand Down
52 changes: 35 additions & 17 deletions lib/ex_owm/api.ex
Original file line number Diff line number Diff line change
@@ -1,47 +1,65 @@
defmodule ExOwm.Api do
@moduledoc """
This module contains OpenWeatherMap API related functions.
This module contains functions for interacting with the OpenWeatherMap API.
It prepares request strings, makes API calls, and parses the responses.
"""
alias ExOwm.RequestString
alias HTTPoison.{Error, Response}

@doc """
Prepares request string basing on given params, calls OWM API, parses and
decodes the answers.
Prepares a request string based on the given parameters, calls the OWM API,
and parses the JSON response.
## Parameters
- `api_call_type` (atom): The type of API call (e.g., `:get_weather`, `:get_current_weather`).
- `location` (map): The location parameters (e.g., city, coordinates, zip code).
- `opts` (term): Optional parameters for the API call (e.g., type, mode, units, cnt, lang).
## Returns
- (map): The parsed JSON response.
- `{:error, term, term}`: An error tuple containing the error type and the response.
"""
@spec send_and_parse_request(atom, map, term) :: map | {:error, term, term}
def send_and_parse_request(api_call_type, location, opts) do
RequestString.build(api_call_type, location, opts)
api_call_type
|> RequestString.build(location, opts)
|> call_api()
|> parse_json()
|> parse_response()
end

defp call_api(string) do
case HTTPoison.get(string) do
{:ok, %HTTPoison.Response{status_code: 200, body: json_body}} ->
@spec call_api(String.t()) :: {:ok, String.t()} | {:error, atom, term} | {:error, term}
defp call_api(url) do
case HTTPoison.get(url) do
{:ok, %Response{status_code: 200, body: json_body}} ->
{:ok, json_body}

{:ok, %HTTPoison.Response{status_code: 404, body: json_body}} ->
{:ok, %Response{status_code: 404, body: json_body}} ->
{:error, :not_found, json_body}

{:ok, %HTTPoison.Response{status_code: 400, body: json_body}} ->
{:error, :not_found, json_body}
{:ok, %Response{status_code: 400, body: json_body}} ->
{:error, :bad_request, json_body}

{:ok, %HTTPoison.Response{status_code: 401, body: json_body}} ->
{:ok, %Response{status_code: 401, body: json_body}} ->
{:error, :api_key_invalid, json_body}

{:ok, response} ->
{:error, :unknown_api_response, response}

{:error, reason} ->
{:error, %Error{} = reason} ->
{:error, reason}
end
end

defp parse_json({:ok, json}), do: Jason.decode(json)
@spec parse_response({:ok, String.t()} | {:error, atom, String.t()} | {:error, term}) ::
map | {:error, term, term}
defp parse_response({:ok, json}), do: Jason.decode(json)

defp parse_json({:error, :unknown_api_response, response}),
defp parse_response({:error, :unknown_api_response, response}),
do: {:error, :unknown_api_response, response}

defp parse_json({:error, reason, json_body}), do: {:error, reason, Jason.decode!(json_body)}
defp parse_json({:error, %HTTPoison.Error{} = reason}), do: {:error, reason}
defp parse_response({:error, reason, json_body}), do: {:error, reason, Jason.decode(json_body)}

defp parse_response({:error, %Error{} = reason}), do: {:error, reason}
end
1 change: 1 addition & 0 deletions lib/ex_owm/cache.ex
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
defmodule ExOwm.Cache do
@moduledoc false
use Nebulex.Cache, otp_app: :ex_owm, adapter: Nebulex.Adapters.Local
end
8 changes: 4 additions & 4 deletions lib/ex_owm/current_weather/worker.ex
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,17 @@ defmodule ExOwm.CurrentWeather.Worker do
@moduledoc """
Current Weather Worker task implementation.
"""
alias ExOwm.Api
alias ExOwm.Cache
alias ExOwm.{Api, WorkerHelper}

@doc """
Returns current weather for a specific location and given options.
Checks whether request has been already cached, if not it sends the request to
OWM API and caches it with specific TTL.
"""
@spec get_current_weather(map, key: atom) :: map
@spec get_current_weather(map, key: atom) ::
{:ok, map()} | {:error, map()} | {:error, map(), map()}
def get_current_weather(location, opts) do
ExOwm.WorkerHelper.get_from_cache_or_call("current_weather: #{inspect(location)}", fn ->
WorkerHelper.get_from_cache_or_call("current_weather: #{inspect(location)}", fn ->
Api.send_and_parse_request(:get_current_weather, location, opts)
end)
end
Expand Down
8 changes: 4 additions & 4 deletions lib/ex_owm/five_day_forecast/worker.ex
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,17 @@ defmodule ExOwm.FiveDayForecast.Worker do
@moduledoc """
Five Day Forecast Worker task implementation.
"""
alias ExOwm.Api
alias ExOwm.Cache
alias ExOwm.{Api, WorkerHelper}

@doc """
Returns five day weather forecast for a specific location and given options.
Checks whether request has been already cached, if not it sends the request to
OWM API and caches it with specific TTL.
"""
@spec get_five_day_forecast(map, key: atom) :: map
@spec get_five_day_forecast(map, key: atom) ::
{:ok, map()} | {:error, map()} | {:error, map(), map()}
def get_five_day_forecast(location, opts) do
ExOwm.WorkerHelper.get_from_cache_or_call("five_day_forecast: #{inspect(location)}", fn ->
WorkerHelper.get_from_cache_or_call("five_day_forecast: #{inspect(location)}", fn ->
Api.send_and_parse_request(:get_five_day_forecast, location, opts)
end)
end
Expand Down
8 changes: 4 additions & 4 deletions lib/ex_owm/historical_weather/worker.ex
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,17 @@ defmodule ExOwm.HistoricalWeather.Worker do
@moduledoc """
Five Day Forecast Worker task implementation.
"""
alias ExOwm.Api
alias ExOwm.Cache
alias ExOwm.{Api, WorkerHelper}

@doc """
Returns five day weather forecast for a specific location and given options.
Checks whether request has been already cached, if not it sends the request to
OWM API and caches it with specific TTL.
"""
@spec get_historical_weather(map, key: atom) :: map
@spec get_historical_weather(map, key: atom) ::
{:ok, map()} | {:error, map()} | {:error, map(), map()}
def get_historical_weather(location, opts) do
ExOwm.WorkerHelper.get_from_cache_or_call("historical_weather: #{inspect(location)}", fn ->
WorkerHelper.get_from_cache_or_call("historical_weather: #{inspect(location)}", fn ->
Api.send_and_parse_request(:get_historical_weather, location, opts)
end)
end
Expand Down
8 changes: 4 additions & 4 deletions lib/ex_owm/hourly_forecast/worker.ex
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,17 @@ defmodule ExOwm.HourlyForecast.Worker do
@moduledoc """
Five Day Forecast Worker task implementation.
"""
alias ExOwm.Api
alias ExOwm.Cache
alias ExOwm.{Api, WorkerHelper}

@doc """
Returns five day weather forecast for a specific location and given options.
Checks whether request has been already cached, if not it sends the request to
OWM API and caches it with specific TTL.
"""
@spec get_hourly_forecast(map, key: atom) :: map
@spec get_hourly_forecast(map, key: atom) ::
{:ok, map()} | {:error, map()} | {:error, map(), map()}
def get_hourly_forecast(location, opts) do
ExOwm.WorkerHelper.get_from_cache_or_call("hourly_forecast: #{inspect(location)}", fn ->
WorkerHelper.get_from_cache_or_call("hourly_forecast: #{inspect(location)}", fn ->
Api.send_and_parse_request(:get_hourly_forecast, location, opts)
end)
end
Expand Down
Loading

0 comments on commit 59c89db

Please sign in to comment.