Alpr.sh SDK para Elixir

SDK oficial da API da Alpr.sh para Elixir 1.18+, sem dependências: só :httpc, :crypto e o módulo JSON da biblioteca padrão. É gerado a partir do contrato da API (openapi.json, na raiz de alpr-sh/sdk) por generator/. Não edite os arquivos desta pasta: regenere.

# mix.exs
{:alpr_sdk, "~> 0.1.0"}

Começando

A chave de API (alpr_live_…) é a do seu espaço de trabalho; não há ambiente de teste. Guarde-a no ambiente, nunca no código.

client = Alpr.new(System.fetch_env!("ALPR_API_KEY"))

# Ler as placas de uma foto.
{:ok, result} = Alpr.Photos.recognize(client, image: File.read!("carro.jpg"), lpr_confidence: 0.5)
IO.puts("#{hd(result.plates).plate} (créditos: #{result.credits_remaining})")

# Percorrer as leituras, da mais recente para a mais antiga.
client
|> Alpr.Recognitions.list_stream(plate: "BRA")
|> Stream.take(100)
|> Enum.each(&IO.puts("#{&1.plate} #{&1.captured_at}"))

# Listar as câmeras.
{:ok, cameras} = Alpr.Cameras.list(client)

# O link temporário da imagem de uma leitura.
{:ok, link} = Alpr.Recognitions.image(client, "det_123")
IO.puts("#{link.image_url} (vale #{link.expires_in} s)")

Recursos

Módulo Funções
Alpr.Cameras list
Alpr.Photos detect, ocr, recognize
Alpr.Recognitions image, list, list_stream

Cada função recebe o cliente primeiro e devolve {:ok, valor} ou {:error, %Alpr.Error{}}. Os modelos são structs com os nomes da API em snake_case (result.credits_remaining, link.image_url); um campo que a resposta não traz ou traz null é nil. Alpr.Cameras.list/2 devolve a lista de câmeras, já sem o envelope data.

Alpr.new/2 aceita base_url:, timeout: (30 segundos, em milissegundos) e transport:. O endereço deve ser https; http só vale para localhost, 127.0.0.0/8 e ::1, e qualquer outro levanta ArgumentError ao criar o cliente, para a chave nunca trafegar em texto claro. Redirecionamentos nunca são seguidos: um 3xx volta como erro. Toda função aceita api_key: para usar a chave de outro espaço de trabalho numa chamada. Não há repetição automática de chamadas.

Foto

recognize, detect e ocr enviam a foto como multipart/form-data. Passe os bytes da foto (um binário) no campo image, como uma keyword list, um mapa com chaves atom ou a struct de parâmetros (%Alpr.PhotosRecognizeParams{image: bytes}). O nome e o tipo do arquivo saem dos primeiros bytes: JPEG (FF D8 FF) vai como photo.jpg (image/jpeg), PNG como photo.png (image/png) e o resto como photo (application/octet-stream), para a API recusar. Os campos opcionais lpr_confidence e min_ocr_confidence (de 0 a 1) só vão quando informados; ocr recebe só a imagem.

Leituras

Alpr.Recognitions.list/2 devolve uma página, %Alpr.Page{items: [...], next: cursor}, em que next é nil na última. Os filtros são plate:, camera: e source:, e before: é o cursor (sem ele, a página mais recente). Alpr.Recognitions.list_stream/2 devolve um Stream preguiçoso que parte do before: dado, entrega cada leitura, segue o next até o fim, para se o cursor não andar e levanta Alpr.Error na primeira recusa.

Erros

Uma recusa da API é um {:error, %Alpr.Error{}}, com status, code, message, fields (um mapa, vazio a menos que a resposta traga fields) e, num 429, retry_after em segundos (do cabeçalho Retry-After). Uma resposta de erro que não é JSON continua sendo um erro, só com o status. Uma chamada que não chegou à API tem status: nil e code: "transport".

Código Status
invalid_api_key 401
insufficient_scope 403
insufficient_credits 402
missing image, invalid_image, invalid_option 400
image_too_large 413
no_plate 422
not_found, image_unavailable 404
rate_limited 429, com Retry-After
engine_unavailable, storage_unavailable 503

Webhooks

{:ok, event} =
  Alpr.verify_webhook(System.fetch_env!("ALPR_WEBHOOK_SECRET"), conn.req_headers, raw_body)

case event do
  %Alpr.RecognitionCreatedWebhookEvent{plate: plate, camera_id: camera} -> IO.puts("#{plate} em #{camera}")
  %Alpr.AlertTriggeredWebhookEvent{rule_name: rule} -> IO.puts("alerta: #{rule}")
  %Alpr.TestWebhookEvent{} -> :ok
  %Alpr.UnknownWebhookEvent{event: name} -> IO.puts("evento novo: #{name}")
end

A Alpr.sh assina cada entrega com três cabeçalhos (a caixa das letras não importa):

Cabeçalho Conteúdo
X-Alprsh-Delivery-Id o id da entrega
X-Alprsh-Timestamp segundos Unix do envio, só dígitos
X-Alprsh-Signature sha256= e os 64 caracteres hexadecimais do HMAC-SHA256

O texto assinado é timestamp + "." + delivery-id + "." + corpo, e a chave é o texto do segredo como ele é, whsec_… inclusive (não é decodificado). Passe o corpo cru, exatamente como chegou, antes de interpretá-lo; headers pode ser um mapa ou uma lista de pares (conn.req_headers).

Durante a rotação do segredo o cabeçalho traz uma assinatura por segredo, num valor separado por vírgulas ou em linhas repetidas, e basta uma conferir. São aceitas até 6; uma assinatura que não seja sha256= e 64 caracteres hexadecimais recusa a requisição inteira. O timestamp precisa estar a no máximo 300 segundos de agora (Alpr.Webhook.tolerance_seconds/0), para mais ou para menos.

Uma assinatura que não confere devolve {:error, %Alpr.WebhookVerificationError{}}; um corpo assinado que não é um evento (não é um objeto JSON, ou não tem event) devolve {:error, %Alpr.WebhookParseError{}}; um segredo vazio é erro de configuração e levanta ArgumentError. Alpr.verify_webhook_signature/4 só confere a assinatura, sem ler o corpo.

O evento é tipado, com os campos do corpo no próprio struct, sem data nem type: event diz qual evento é (recognition.created chega como RecognitionCreatedWebhookEvent, alert.triggered como AlertTriggeredWebhookEvent, webhook.test como TestWebhookEvent). Um evento que esta versão do SDK ainda não conhece não é um erro: chega como UnknownWebhookEvent, com event, id (vazio se o corpo não traz um id de texto), body (o corpo decodificado) e raw (o corpo como foi enviado).

Deduplique as entregas pelo cabeçalho X-Alprsh-Delivery-Id. O id do corpo é o da leitura ou do alerta, não o da entrega.

Versão do SDK: 0.1.0 · contrato da API: 1.0.0