Numista API
This commit is contained in:
Generated
+1
@@ -1275,6 +1275,7 @@ dependencies = [
|
||||
"dotenv",
|
||||
"log",
|
||||
"once_cell",
|
||||
"regex",
|
||||
"reqwest",
|
||||
"sea-orm",
|
||||
"serde",
|
||||
|
||||
@@ -19,6 +19,7 @@ chrono = { version = "^0.4", features = ["serde"] }
|
||||
dotenv = "^0.15"
|
||||
log = "^0"
|
||||
once_cell = "^1"
|
||||
regex = "^1"
|
||||
reqwest = { version = "^0.11", features = ["json"] }
|
||||
sea-orm = { version = "^0", features = [
|
||||
"sqlx-postgres",
|
||||
|
||||
@@ -9,6 +9,11 @@ CREATE TABLE coins
|
||||
max_year integer,
|
||||
obverse_thumbnail text,
|
||||
reverse_thumbnail text,
|
||||
weight real NOT NULL,
|
||||
diameter real NOT NULL,
|
||||
thickness real NOT NULL,
|
||||
material character(3) NOT NULL,
|
||||
purity real NOT NULL,
|
||||
PRIMARY KEY (id)
|
||||
);
|
||||
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="1235" height="650" viewBox="0 0 36.1 19" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<defs>
|
||||
<path id="a" transform="scale(.58515)" d="m0-1 0.58779 1.809-1.5388-1.118h1.9021l-1.5388 1.118z" fill="#fff"/>
|
||||
<g id="b">
|
||||
<use x=".9025" y="0.8501" xlink:href="#a"/>
|
||||
<use x="2.7075" y="0.8501" xlink:href="#a"/>
|
||||
<use x="4.5125" y="0.8501" xlink:href="#a"/>
|
||||
<use x="6.3175" y="0.8501" xlink:href="#a"/>
|
||||
<use x="8.1225" y="0.8501" xlink:href="#a"/>
|
||||
<use x="9.9275" y="0.8501" xlink:href="#a"/>
|
||||
<use x="11.7325" y="0.8501" xlink:href="#a"/>
|
||||
<use x="13.5375" y="0.8501" xlink:href="#a"/>
|
||||
</g>
|
||||
<g id="c">
|
||||
<use xlink:href="#b"/>
|
||||
<use y="1.705" xlink:href="#b"/>
|
||||
<use y="3.41" xlink:href="#b"/>
|
||||
<use y="5.115" xlink:href="#b"/>
|
||||
<use y="6.82" xlink:href="#b"/>
|
||||
<use y="8.525" xlink:href="#b"/>
|
||||
</g>
|
||||
</defs>
|
||||
<rect width="36.1" height="19" fill="#BB133E"/>
|
||||
<path d="m0 2.1922h36.1m-36.1 2.923h36.1m-36.1 2.923h36.1m-36.1 2.923h36.1m-36.1 2.923h36.1m-36.1 2.923h36.1" stroke="#FFF" stroke-width="1.4615"/>
|
||||
<rect width="14.44" height="10.23" fill="#002664"/>
|
||||
<use xlink:href="#c"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="1235" height="650" viewBox="0 0 7410 3900" xmlns:v="https://vecta.io/nano"><path fill="#b22234" d="M0 0h7410v3900H0z"/><path d="M0,450H7410m0,600H0m0,600H7410m0,600H0m0,600H7410m0,600H0" stroke="#fff" stroke-width="300"/><path fill="#3c3b6e" d="M0 0h2964v2100H0z"/><g fill="#fff"><g id="A"><g id="B"><g id="C"><g id="D"><path id="E" d="M247 90l70.534 217.082-184.661-134.164h228.254L176.466 307.082z"/><use xlink:href="#E" y="420"/><use xlink:href="#E" y="840"/><use xlink:href="#E" y="1260"/></g><use xlink:href="#E" y="1680"/></g><use xlink:href="#D" x="247" y="210"/></g><use xlink:href="#B" x="494"/></g><use xlink:href="#A" x="988"/><use xlink:href="#B" x="1976"/><use xlink:href="#C" x="2470"/></g></svg>
|
||||
|
After Width: | Height: | Size: 800 B |
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg viewBox="0 0 601 601" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="300.5" cy="300.5" r="300" fill="#1a1e3b"/>
|
||||
<path d="m300.5 0.5 67.36 207.29 217.96 8e-3 -176.33 128.12 67.345 207.29-176.34-128.1-176.34 128.1 67.345-207.29-176.33-128.12 217.96-8e-3z" fill="#fff" fill-rule="evenodd"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 340 B |
@@ -0,0 +1,23 @@
|
||||
.flag {
|
||||
height: 1.4em;
|
||||
vertical-align: bottom;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.coins {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
td.thumbnail {
|
||||
width: 90px;
|
||||
}
|
||||
|
||||
img.thumbnail {
|
||||
height: 90px;
|
||||
width: 90px;
|
||||
}
|
||||
|
||||
caption {
|
||||
caption-side: bottom;
|
||||
text-align: left;
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
@use 'generic';
|
||||
@use 'layout';
|
||||
@use 'nav';
|
||||
@use 'nav';
|
||||
@use 'guide';
|
||||
+155
@@ -1,3 +1,10 @@
|
||||
use crate::db::get_db_pool;
|
||||
use crate::orm::coins;
|
||||
use regex::Regex;
|
||||
use sea_orm::{prelude::*, query::*, ActiveValue, DatabaseConnection, EntityTrait};
|
||||
use serde::{Deserialize, Deserializer, Serialize};
|
||||
use std::collections::HashMap;
|
||||
|
||||
// https://en.numista.com/api/doc/index.php
|
||||
//{
|
||||
// "id": 38581,
|
||||
@@ -12,3 +19,151 @@
|
||||
// "obverse_thumbnail": "https://en.numista.com/catalogue/photos/abkhazia/98-180.jpg",
|
||||
// "reverse_thumbnail": "https://en.numista.com/catalogue/photos/abkhazia/99-180.jpg"
|
||||
//},
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
pub struct Coin {
|
||||
pub id: i32,
|
||||
pub title: String,
|
||||
pub issuer: Issuer,
|
||||
|
||||
pub min_year: i32,
|
||||
pub max_year: i32,
|
||||
|
||||
pub obverse: Engraving,
|
||||
pub reverse: Engraving,
|
||||
|
||||
pub weight: f32,
|
||||
pub size: f32,
|
||||
pub thickness: f32,
|
||||
pub composition: Composition,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
pub struct Issuer {
|
||||
pub code: String,
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
pub struct Engraving {
|
||||
pub thumbnail: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct Composition {
|
||||
pub material: String,
|
||||
pub purity: Option<f32>,
|
||||
}
|
||||
|
||||
impl Composition {
|
||||
pub fn to_ticker(&self) -> String {
|
||||
match self.material.as_str() {
|
||||
"Gold" => "XAU",
|
||||
"Silver" => "XAG",
|
||||
_ => unreachable!(),
|
||||
}
|
||||
.to_owned()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for Composition {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct CompositionWrapper {
|
||||
text: String,
|
||||
}
|
||||
|
||||
let wrapper: CompositionWrapper = Deserialize::deserialize(deserializer)?;
|
||||
|
||||
let regex = Regex::new(r"(Gold|Silver)(?:\s\(\.(\d{3})\))").unwrap();
|
||||
let caps = regex.captures(&wrapper.text).unwrap();
|
||||
|
||||
Ok(Composition {
|
||||
material: caps.get(1).unwrap().as_str().to_owned(),
|
||||
purity: caps
|
||||
.get(2)
|
||||
.map(|v| v.as_str().parse::<u32>().unwrap() as f32 / 1000.0),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Numista IDs for relevant US coins.
|
||||
pub const USA_COINS: [u32; 15] = [
|
||||
1492, 10085, 5830, 10591, 30785, 51, 4455, 3573, 5580, 54, 45, 52, 2835, 943, 10865,
|
||||
];
|
||||
|
||||
pub async fn init_coins(db: &DatabaseConnection) {
|
||||
let api_key = std::env::var("NUMISTA_API_KEY").expect("NUMISTA_API_KEY must be set.");
|
||||
let new_coins: Vec<coins::Model> = Vec::with_capacity(USA_COINS.len());
|
||||
|
||||
let usa_coins = coins::Entity::find()
|
||||
.filter(coins::Column::NumistaId.is_in(USA_COINS))
|
||||
.all(get_db_pool())
|
||||
.await
|
||||
.expect("Couldn't query coins from database")
|
||||
.into_iter()
|
||||
.map(|coin| (coin.numista_id, coin))
|
||||
.collect::<HashMap<_, _>>();
|
||||
|
||||
for coin in USA_COINS {
|
||||
if !usa_coins.contains_key(&(coin as i32)) {
|
||||
let coin = reqwest::Client::new()
|
||||
.get(format!(
|
||||
"https://api.numista.com/api/v3/types/{}?lang=en",
|
||||
coin
|
||||
))
|
||||
.header(reqwest::header::EXPECT, "application/json")
|
||||
.header("Numista-API-Key", api_key.to_owned())
|
||||
.send()
|
||||
.await
|
||||
.expect("Failed to query Numista API.")
|
||||
.json::<Coin>()
|
||||
.await
|
||||
.expect("Failed to deserialize Numista API result.");
|
||||
|
||||
log::debug!("Pulled new Numista coin: {:?}", coin);
|
||||
|
||||
let new_coin = coins::ActiveModel {
|
||||
id: ActiveValue::NotSet,
|
||||
numista_id: ActiveValue::set(coin.id),
|
||||
issuer: ActiveValue::set(coin.issuer.code),
|
||||
title: ActiveValue::set(coin.title),
|
||||
min_year: ActiveValue::set(Some(coin.min_year)),
|
||||
max_year: ActiveValue::set(Some(coin.max_year)),
|
||||
obverse_thumbnail: ActiveValue::set(Some(coin.obverse.thumbnail)),
|
||||
reverse_thumbnail: ActiveValue::set(Some(coin.reverse.thumbnail)),
|
||||
weight: ActiveValue::set(coin.weight),
|
||||
diameter: ActiveValue::set(coin.size),
|
||||
thickness: ActiveValue::set(coin.thickness),
|
||||
material: ActiveValue::set(coin.composition.to_ticker()),
|
||||
purity: ActiveValue::set(coin.composition.purity.unwrap_or(1.0)),
|
||||
}
|
||||
.insert(db)
|
||||
.await
|
||||
.expect("Failed to insert a new coin.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[test]
|
||||
fn test_composition_deserialize() {
|
||||
let silver = r#"{ "text": "Silver (.400)" }"#;
|
||||
let silver_comp: super::Composition =
|
||||
serde_json::from_str(silver).expect("Failed to parse silver");
|
||||
|
||||
assert_eq!(silver_comp.material, "Silver");
|
||||
assert_eq!(silver_comp.purity, Some(0.4));
|
||||
|
||||
let gold = r#"{ "text": "Gold (.900)" }"#;
|
||||
let gold_comp: super::Composition =
|
||||
serde_json::from_str(gold).expect("Failed to parse gold");
|
||||
|
||||
assert_eq!(gold_comp.material, "Gold");
|
||||
assert_eq!(gold_comp.purity, Some(0.9));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
pub mod usa;
|
||||
@@ -0,0 +1,50 @@
|
||||
use crate::coin::USA_COINS;
|
||||
use crate::db::get_db_pool;
|
||||
use crate::middleware::ClientCtx;
|
||||
use crate::orm::coins;
|
||||
use actix_web::{error, get, Error, Responder};
|
||||
use askama_actix::Template;
|
||||
use sea_orm::{prelude::*, EntityTrait};
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[derive(Template)]
|
||||
#[template(path = "part/coin.html")]
|
||||
pub struct CoinTemplate<'a> {
|
||||
pub client: &'a ClientCtx,
|
||||
pub coin: &'a coins::Model,
|
||||
}
|
||||
|
||||
#[derive(Template)]
|
||||
#[template(path = "guides/usa.html")]
|
||||
pub struct GuideTemplate {
|
||||
pub client: ClientCtx,
|
||||
pub coins: HashMap<i32, coins::Model>,
|
||||
}
|
||||
|
||||
impl GuideTemplate {
|
||||
pub fn show_coin(&self, id: i32) -> String {
|
||||
match self.coins.get(&id) {
|
||||
Some(coin) => CoinTemplate {
|
||||
client: &self.client,
|
||||
coin: &coin,
|
||||
}
|
||||
.render()
|
||||
.unwrap_or("<tr>Failed to render coin row.</tr>".to_string()),
|
||||
None => format!("<tr>Requested coin `N{}` not in database.</tr>", id),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[get("/guide/usa")]
|
||||
pub async fn view_index(client: ClientCtx) -> Result<impl Responder, Error> {
|
||||
let coins = coins::Entity::find()
|
||||
.filter(coins::Column::NumistaId.is_in(USA_COINS))
|
||||
.all(get_db_pool())
|
||||
.await
|
||||
.map_err(error::ErrorInternalServerError)?
|
||||
.into_iter()
|
||||
.map(|coin| (coin.numista_id, coin))
|
||||
.collect::<HashMap<_, _>>();
|
||||
|
||||
Ok(GuideTemplate { client, coins })
|
||||
}
|
||||
+7
-1
@@ -1,8 +1,11 @@
|
||||
use crate::coin::init_coins;
|
||||
use crate::db::init_db_pool;
|
||||
use crate::price::init_spot_prices;
|
||||
use actix_web::{web::Data, App, HttpServer};
|
||||
|
||||
mod coin;
|
||||
mod db;
|
||||
mod guide;
|
||||
mod middleware;
|
||||
mod orm;
|
||||
mod price;
|
||||
@@ -13,7 +16,8 @@ async fn main() -> std::io::Result<()> {
|
||||
dotenv::dotenv().expect("Could not open .env file");
|
||||
|
||||
let db = init_db_pool().await;
|
||||
let prices = init_spot_prices(db).await;
|
||||
init_spot_prices(db).await;
|
||||
init_coins(db).await;
|
||||
|
||||
// Recast db in a Data wrapper.
|
||||
let db = Data::new(db);
|
||||
@@ -23,6 +27,8 @@ async fn main() -> std::io::Result<()> {
|
||||
.app_data(Data::clone(&db))
|
||||
.service(crate::web::view_css)
|
||||
.service(crate::web::view_index)
|
||||
.service(crate::web::view_flag)
|
||||
.service(crate::guide::usa::view_index)
|
||||
})
|
||||
.bind(("127.0.0.1", 8080))?
|
||||
.run()
|
||||
|
||||
@@ -18,6 +18,13 @@ pub struct Model {
|
||||
pub obverse_thumbnail: Option<String>,
|
||||
#[sea_orm(column_type = "Text", nullable)]
|
||||
pub reverse_thumbnail: Option<String>,
|
||||
|
||||
weight: f32,
|
||||
diameter: f32,
|
||||
thickness: f32,
|
||||
|
||||
material: String,
|
||||
purity: f32,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter)]
|
||||
|
||||
+2
-5
@@ -1,12 +1,9 @@
|
||||
use crate::{db::get_db_pool, orm::spots};
|
||||
use actix_web::web::Data;
|
||||
use crate::orm::spots;
|
||||
use chrono::NaiveDateTime;
|
||||
use sea_orm::{prelude::*, query::*, ActiveValue, DatabaseConnection, EntityTrait};
|
||||
use serde::Deserialize;
|
||||
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
|
||||
|
||||
/// Const for parsing atomic values to real currency.
|
||||
pub const ATOMIC_TO_CURRENCY: f32 = 0.001;
|
||||
/// Mathematical constant for convering 1 troy ounce to grams.
|
||||
pub const TROY_OUNCE_TO_GRAM: f32 = 31.1035;
|
||||
|
||||
@@ -229,7 +226,7 @@ pub async fn store_spot_prices(db: &DatabaseConnection, api: ApiData) -> Result<
|
||||
};
|
||||
|
||||
spots::Entity::insert_many(vec![copper, silver, gold])
|
||||
.exec(get_db_pool())
|
||||
.exec(db)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
|
||||
+11
-1
@@ -1,7 +1,8 @@
|
||||
use crate::middleware::ClientCtx;
|
||||
use actix_files::NamedFile;
|
||||
use actix_web::{get, Error, Responder};
|
||||
use actix_web::{get, Error, HttpRequest, Responder, Result};
|
||||
use askama_actix::Template;
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[derive(Template)]
|
||||
#[template(path = "index.html")]
|
||||
@@ -19,3 +20,12 @@ pub async fn view_css() -> Result<NamedFile, Error> {
|
||||
let path = "public/assets/style.css";
|
||||
Ok(NamedFile::open(path)?)
|
||||
}
|
||||
|
||||
#[get("/images/flags/{filename:.*}")]
|
||||
pub async fn view_flag(req: HttpRequest) -> Result<NamedFile> {
|
||||
let filename: PathBuf = req.match_info().query("filename").parse().unwrap();
|
||||
Ok(NamedFile::open(format!(
|
||||
"public/images/flags/{}",
|
||||
filename.to_str().expect("No filename.")
|
||||
))?)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
{% extends "container/public.html" %}
|
||||
|
||||
{% block content %}
|
||||
<section>
|
||||
<h1>United States Constitutional Coins</h1>
|
||||
<p>
|
||||
On April 2nd, 1972, the 2nd United States Congress passed the Coinage Act of 1972.
|
||||
This officially defined the currency of the United States as gold, silver, and copper.
|
||||
The Gold Eagle equaled 247 ½ grains (17.5g), the Silver Dollar equaled 371 ¼ grains (27.0g), and the Copper Cent
|
||||
was 11 pennyweights (17.1g).
|
||||
</p>
|
||||
<p>This would be the definition of money in the country until February 12th, 1873.</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>
|
||||
Post-Civil War
|
||||
<img class="flag" src="/images/flags/usa-48.svg" />
|
||||
</h2>
|
||||
<table class="coins">
|
||||
<caption>
|
||||
Though silver had been officially demonetized, it remained in currency for almost a century after.
|
||||
</caption>
|
||||
<thead>
|
||||
<tr>
|
||||
<th colspan="2">
|
||||
<!-- Portrait -->
|
||||
</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{ self.show_coin(1492)|safe }}
|
||||
{{ self.show_coin(10085)|safe }}
|
||||
{{ self.show_coin(5830)|safe }}
|
||||
{{ self.show_coin(10591)|safe }}
|
||||
{{ self.show_coin(30785)|safe }}
|
||||
{{ self.show_coin(51)|safe }}
|
||||
{{ self.show_coin(4455)|safe }}
|
||||
{{ self.show_coin(3573)|safe }}
|
||||
{{ self.show_coin(5580)|safe }}
|
||||
{{ self.show_coin(54)|safe }}
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>
|
||||
World War 2 Coins
|
||||
<img class="flag" src="/images/flags/usa-roundel.svg" />
|
||||
</h2>
|
||||
<table class="coins">
|
||||
<caption>
|
||||
During the Second World War, nickel became a strategic material used as an alloy in materiel and
|
||||
industrial equipment. The U.S. would produce Nickels with 35% silver content between 1942 and 1945.
|
||||
</caption>
|
||||
<thead>
|
||||
<tr>
|
||||
<th colspan="2">
|
||||
<!-- Portrait -->
|
||||
</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{ self.show_coin(45)|safe }}
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>
|
||||
Post-War Coins
|
||||
<img class="flag" src="/images/flags/usa-50.svg" />
|
||||
</h2>
|
||||
<table class="coins">
|
||||
<caption>
|
||||
The U.S. kept silver in its currency until the year before President Nixon ended Bretton-Woods and
|
||||
officially removed the U.S. Dollar from the gold standard.
|
||||
</caption>
|
||||
<thead>
|
||||
<tr>
|
||||
<th colspan="2">
|
||||
<!-- Portrait -->
|
||||
</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{ self.show_coin(52)|safe }}
|
||||
{{ self.show_coin(2835)|safe }}
|
||||
{{ self.show_coin(943)|safe }}
|
||||
{{ self.show_coin(10865)|safe }}
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,15 @@
|
||||
<tr class="coin">
|
||||
<td class="thumbnail">
|
||||
{% if let Some(thumbnail) = coin.obverse_thumbnail %}
|
||||
<img class="thumbnail observe" src="{{ thumbnail }}" />
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="thumbnail">
|
||||
{% if let Some(thumbnail) = coin.reverse_thumbnail %}
|
||||
<img class="thumbnail reverse" src="{{ thumbnail }}" />
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<h3>{{ coin.title }}</h3>
|
||||
</td>
|
||||
</tr>
|
||||
Reference in New Issue
Block a user