Packages

Download and stream historical price data for variety of financial instruments (Forex, Commodities and Indices) from Dukascopy Bank SA.

Current section

Files

Jump to
dukascopy lib dukascopy client.ex
Raw

lib/dukascopy/client.ex

defmodule Dukascopy.Client do
@moduledoc false
# HTTP client for Dukascopy API using Req.
# Handles retry, caching, and LZMA decompression via Req steps.
@base_url "https://datafeed.dukascopy.com/datafeed"
@default_cache_folder_path ".dukascopy-cache"
@client_options [
:plug,
:max_retries,
:retry_delay,
:retry_on_empty,
:retry_log_level,
:use_cache,
:cache_folder_path,
:fail_after_retry_count
]
## Public API
@doc false
@spec base_url() :: String.t()
def base_url(), do: @base_url
@doc false
@spec fetch(String.t(), Keyword.t()) :: {:ok, binary()} | {:error, term()}
def fetch(path, opts \\ []) do
opts
|> build_request()
|> Req.get(url: path)
|> handle_response(opts)
end
## Private functions
defp build_request(opts) do
client_opts = Keyword.take(opts, @client_options)
Req.new(
base_url: @base_url,
retry: &should_retry/2,
decode_body: false,
compressed: false
)
|> Req.Request.register_options(@client_options)
|> Req.Request.merge_options(client_opts)
|> maybe_add_cache_steps(client_opts)
|> Req.Request.append_response_steps(decompress_lzma: &decompress_lzma/1)
end
defp should_retry(request, response_or_error) do
retry_on_empty = request.options[:retry_on_empty] || false
case response_or_error do
%Req.Response{status: 200, body: <<>>} when retry_on_empty -> true
%Req.Response{status: 200} -> false
%Req.Response{status: 404} -> false
%Req.Response{} -> true
%{__exception__: true} -> true
_ -> false
end
end
defp decompress_lzma({request, %Req.Response{status: 200, body: body} = response})
when byte_size(body) > 0 do
case LZMA.lzma_decompress(body) do
{:ok, decompressed} ->
{request, %Req.Response{response | body: decompressed}}
{:error, reason} ->
{request, RuntimeError.exception("LZMA decompression failed: #{reason}")}
end
end
defp decompress_lzma({request, response}), do: {request, response}
defp maybe_add_cache_steps(req, opts) do
if Keyword.get(opts, :use_cache, false) do
req
|> Req.Request.prepend_request_steps(cache_read: &cache_read/1)
|> Req.Request.append_response_steps(cache_write: &cache_write/1)
else
req
end
end
defp cache_read(request) do
cache_path = request.options[:cache_folder_path] || @default_cache_folder_path
cache_file = cache_file_path(request.url, cache_path)
case File.read(cache_file) do
{:ok, data} -> {request, Req.Response.new(status: 200, body: data)}
{:error, :enoent} -> request
end
end
defp cache_write({request, %Req.Response{status: 200, body: body} = response})
when byte_size(body) > 0 do
cache_path = request.options[:cache_folder_path] || @default_cache_folder_path
cache_file = cache_file_path(request.url, cache_path)
:ok = File.mkdir_p!(cache_path)
:ok = File.write!(cache_file, body)
{request, response}
end
defp cache_write({request, response}), do: {request, response}
defp cache_file_path(url, cache_path) do
cache_key =
url
|> URI.to_string()
|> String.trim_leading(@base_url <> "/")
|> String.replace("/", "-")
Path.join(cache_path, cache_key)
end
defp handle_response({:ok, %Req.Response{status: 200, body: body}}, _opts), do: {:ok, body}
defp handle_response({:ok, %Req.Response{status: 404}}, _opts), do: {:ok, <<>>}
defp handle_response({:ok, %Req.Response{status: status}}, opts) do
case Keyword.get(opts, :fail_after_retry_count, true) do
true -> {:error, {:http_error, status}}
false -> {:ok, <<>>}
end
end
defp handle_response({:error, exception}, opts) do
case Keyword.get(opts, :fail_after_retry_count, true) do
true -> {:error, exception}
false -> {:ok, <<>>}
end
end
end