Skip to content

Commit

Permalink
change "ruler" to "name", closes #50; add Republic coinage to web_scr…
Browse files Browse the repository at this point in the history
…aper ingestion, closes #49
  • Loading branch information
vbalalian committed Dec 19, 2023
1 parent 2db0697 commit afa4470
Show file tree
Hide file tree
Showing 7 changed files with 108 additions and 98 deletions.
32 changes: 16 additions & 16 deletions api/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ async def v1_root() -> JSONResponse:
'status': 'OK',
'documentation': '/docs/',
'endpoints': {
'/v1/coins': 'Retrieve a paginated list of coins with optional sorting and filtering. You can filter by properties like ruler, metal, era, and more, as well as sort the results.',
'/v1/coins': 'Retrieve a paginated list of coins with optional sorting and filtering. You can filter by properties like name, metal, era, and more, as well as sort the results.',
'/v1/coins/id/{coin_id}': 'Retrieve detailed information about a single coin by its ID. This endpoint provides complete data about a specific coin.',
'/v1/coins/search': 'Search for coins based on a query. This allows you to find coins by matching against their descriptions or other text attributes.',
'/v1/coins/id/{coin_id} [POST]': 'Add a new coin to the database. This endpoint is for inserting new coin data into the collection.',
Expand All @@ -51,15 +51,15 @@ async def v1_root() -> JSONResponse:

# Coin validation models
class CoinDetails(BaseModel):
ruler: str | None = Field(default=None, title="The name of the coin's figurehead", max_length=30)
ruler_detail: str | None = Field(default=None, title="The subtitle/detail of the coin's figurehead", max_length=1000)
name: str | None = Field(default=None, title="The name of the coin's figurehead", max_length=30)
name_detail: str | None = Field(default=None, title="The subtitle/detail of the coin's figurehead", max_length=1000)
catalog: str | None = Field(default=None, title="The catalog info associated with the coin", max_length=80)
description: str | None = Field(default=None, title="The description of the coin", max_length=1000)
metal: str | None = Field(default=None, title="The metal/material composition of the coin", max_length=20)
mass: float | None = Field(gt=0.0, lt=50, default=None, title="The mass of the coin in grams")
diameter: float | None = Field(gt=0.0, le=50, default=None, title="The diameter of the coin in millimeters")
era: str | None = Field(default=None, title="The era of the coin e.g. BC or AD")
year: int | None = Field(ge=-50, le=500, default=None, title="The year associated with the coin")
year: int | None = Field(ge=-750, le=500, default=None, title="The year associated with the coin")
inscriptions: str | None = Field(default=None, title="Recognized inscriptions found on the coin")
txt: str | None = Field(default=None, title="Filename of alternate coin information .txt")

Expand All @@ -85,8 +85,8 @@ def validate_era(cls, v):
"json_schema_extra": {
"examples": [
{
"ruler": "Augustus",
"ruler_detail": "The first Roman Emperor, aka Octavian, adopted son of Julius Caesar",
"name": "Augustus",
"name_detail": "The first Roman Emperor, aka Octavian, adopted son of Julius Caesar",
"description": "Denarius, Victory crowning an eagle / Laureate head right",
"catalog": "RIC 248",
"metal": "Silver",
Expand All @@ -110,8 +110,8 @@ class Coin(CoinDetails):
"examples": [
{
"id": "f351566b-7f7b-4ff6-9d90-aac9a09045db",
"ruler": "Augustus",
"ruler_detail": "The first Roman Emperor, aka Octavian, adopted son of Julius Caesar",
"name": "Augustus",
"name_detail": "The first Roman Emperor, aka Octavian, adopted son of Julius Caesar",
"catalog": "RIC 248",
"description": "Denarius, Victory crowning an eagle / Laureate head right",
"metal": "Silver",
Expand Down Expand Up @@ -141,7 +141,7 @@ class PaginatedResponse(BaseModel):

def validate_sort_column(sort_by:str):
'''Validate the sort_by parameter to ensure it's a valid column name'''
allowed_sort_columns = ['ruler', 'catalog', 'metal', 'year', 'mass', 'diameter', 'created', 'modified']
allowed_sort_columns = ['name', 'catalog', 'metal', 'year', 'mass', 'diameter', 'created', 'modified']
if sort_by.lower() not in allowed_sort_columns:
raise HTTPException(status_code=400, detail='Invalid sort column')
return sort_by
Expand All @@ -154,7 +154,7 @@ async def read_coins(
page_size: int = 10,
sort_by: str = None,
desc: bool = False,
ruler: str = None,
name: str = None,
metal: str = None,
era: str = None,
year: str = None,
Expand All @@ -170,8 +170,8 @@ async def read_coins(
end_modified: datetime = None
):

if ruler:
ruler = ruler.title()
if name:
name = name.title()
if metal:
metal = metal.title()
if era:
Expand All @@ -181,7 +181,7 @@ async def read_coins(

# Mapping filters to their SQL query equivalents
filter_mappings = {
'ruler': ('ruler', '=', ruler),
'name': ('name', '=', name),
'metal': ('metal', '=', metal),
'era': ('era', '=', era),
'year': ('year', '=', year),
Expand Down Expand Up @@ -310,7 +310,7 @@ async def add_coin(

# SQL for adding a Coin to the database
insert_query = '''
INSERT INTO roman_coins (id, ruler, ruler_detail, catalog, description, metal, mass, diameter, era, year, inscriptions, txt, created, modified)
INSERT INTO roman_coins (id, name, name_detail, catalog, description, metal, mass, diameter, era, year, inscriptions, txt, created, modified)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
'''

Expand All @@ -319,8 +319,8 @@ async def add_coin(
# Data to be inserted
coin_data = (
coin_id,
coin_details.ruler,
coin_details.ruler_detail,
coin_details.name,
coin_details.name_detail,
coin_details.catalog,
coin_details.description,
coin_details.metal,
Expand Down
50 changes: 25 additions & 25 deletions api/tests/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ def test_database():
test_password = "postgres"
test_host = "test_db"

columns = ['id', 'ruler', 'ruler_detail', 'catalog', 'description',
columns = ['id', 'name', 'name_detail', 'catalog', 'description',
'metal', 'mass', 'diameter', 'era', 'year', 'inscriptions',
'txt', 'created', 'modified']
dtypes = [
Expand Down Expand Up @@ -123,10 +123,10 @@ def test_read_coins(test_client, test_database):
assert response.json()["pagination"] == {"total_items":20, "total_pages":3, "current_page":2, "items_per_page":8}

# Sorting
response = test_client.get("/v1/coins/?sort_by=ruler&desc=True")
response = test_client.get("/v1/coins/?sort_by=name&desc=True")
assert response.status_code == 200
assert len(response.json()["data"]) == 10
assert response.json()["data"][0]["ruler"] == "Hadrian"
assert response.json()["data"][0]["name"] == "Hadrian"

response = test_client.get("/v1/coins/?sort_by=year")
assert response.status_code == 200
Expand All @@ -141,7 +141,7 @@ def test_read_coins(test_client, test_database):
assert response.status_code == 400

# Filtering
response = test_client.get(r"/v1/coins/?ruler=aelia%20Flaccilla&min_diameter=21")
response = test_client.get(r"/v1/coins/?name=aelia%20Flaccilla&min_diameter=21")
assert response.status_code == 200
assert len(response.json()["data"]) == 5

Expand Down Expand Up @@ -178,7 +178,7 @@ def test_search_coins(test_client, test_database):
response = test_client.get(r"/v1/coins/search?query=Hadrian")
assert response.status_code ==200
assert len(response.json()) == 1
assert response.json()[0]["ruler"] == "Hadrian"
assert response.json()[0]["name"] == "Hadrian"

# Query with no results
response = test_client.get(r"/v1/coins/search?query=Caligula")
Expand All @@ -201,7 +201,7 @@ def test_coin_by_id(test_client, test_database):
# Valid ID
response = test_client.get(r"/v1/coins/id/343a3001-ae2e-4745-888e-994374e398a3")
assert response.status_code ==200
assert response.json()["ruler"] == "Aelia Ariadne"
assert response.json()["name"] == "Aelia Ariadne"

# Non-existent ID
response = test_client.get(r"/v1/coins/id/023-450938fgldf-to0r90ftu-438537")
Expand All @@ -221,7 +221,7 @@ def test_coin_by_id(test_client, test_database):
def test_add_coin(test_client, test_database):

# Normal case, no missing fields
coin = {"ruler":"Test Ruler 1", "ruler_detail":"Test Ruler Detail",
coin = {"name":"Test name 1", "name_detail":"Test name Detail",
"catalog":"Test Catalog",
"description":"This is a test description, minimum 50 characters. ",
"metal":"Gold", "mass":8.9, "diameter":20.5, "era":"AD", "year":79,
Expand All @@ -233,7 +233,7 @@ def test_add_coin(test_client, test_database):
# Verify coin added to test database
response = test_client.get(f"/v1/coins/id/{test_id}")
assert response.status_code == 200
assert response.json()["ruler"] == "Test Ruler 1"
assert response.json()["name"] == "Test name 1"
created_at = datetime.strptime(response.json()["created"], r"%Y-%m-%dT%H:%M:%S.%f")
created_truncated = created_at.replace(second=0, microsecond=0)
current_datetime_truncated = datetime.now().replace(second=0, microsecond=0)
Expand All @@ -244,7 +244,7 @@ def test_add_coin(test_client, test_database):
assert modified_truncated == current_datetime_truncated

# Normal case, missing fields
coin = {"ruler":"Test Ruler 2", "catalog":"Test Catalog", "metal":"Gold"}
coin = {"name":"Test name 2", "catalog":"Test Catalog", "metal":"Gold"}
test_id = "this-is-a-test-id-0002"
response = test_client.post(f"/v1/coins/id/{test_id}", json=coin)
assert response.status_code == 201
Expand All @@ -253,7 +253,7 @@ def test_add_coin(test_client, test_database):
# Verify coin added to test database
response = test_client.get(f"/v1/coins/id/{test_id}")
assert response.status_code == 200
assert response.json()["ruler"] == "Test Ruler 2"
assert response.json()["name"] == "Test name 2"
created_at = datetime.strptime(response.json()["created"], r"%Y-%m-%dT%H:%M:%S.%f")
created_truncated = created_at.replace(second=0, microsecond=0)
current_datetime_truncated = datetime.now().replace(second=0, microsecond=0)
Expand All @@ -264,18 +264,18 @@ def test_add_coin(test_client, test_database):
assert modified_truncated == current_datetime_truncated

# Case with invalid data
coin = {"ruler":"Test Ruler 2", "catalog":5, "metal":"Aluminum", "mass":"heavy"}
coin = {"name":"Test name 2", "catalog":5, "metal":"Aluminum", "mass":"heavy"}
test_id = "test-id-0003"
response = test_client.post(f"/v1/coins/id/{test_id}", json=coin)
assert response.status_code == 422

# Case with missing ID
coin = {"ruler":"Test Ruler 2", "catalog":"Test Catalog", "metal":"Gold"}
coin = {"name":"Test name 2", "catalog":"Test Catalog", "metal":"Gold"}
response = test_client.post("/v1/coins/id/", json=coin)
assert response.status_code == 404

# Case with duplicate ID
coin = {"ruler":"Test Ruler 2", "catalog":"Test Catalog", "metal":"Gold"}
coin = {"name":"Test name 2", "catalog":"Test Catalog", "metal":"Gold"}
test_id = "this-is-a-test-id-0002"
response = test_client.post(f"/v1/coins/id/{test_id}", json=coin)
assert response.status_code == 400
Expand All @@ -284,7 +284,7 @@ def test_add_coin(test_client, test_database):
def test_update_coin(test_client, test_database):

# Normal case, full coin update
coin = {"ruler":"Test Ruler 3", "ruler_detail":"Test Ruler Detail",
coin = {"name":"Test name 3", "name_detail":"Test name Detail",
"catalog":"Test Catalog",
"description":"This is a new test description, still 50 characters min.",
"metal":"Silver", "mass":4.2, "diameter":15.5, "era":"BC", "year":-40,
Expand All @@ -296,7 +296,7 @@ def test_update_coin(test_client, test_database):
# Verify coin updated in test database
response = test_client.get(f"/v1/coins/id/{test_id}")
assert response.status_code == 200
assert response.json()["ruler"] == "Test Ruler 3"
assert response.json()["name"] == "Test name 3"
assert response.json()["mass"] == 4.2
assert response.json()["era"] == "BC"
modified_at = datetime.strptime(response.json()["modified"], r"%Y-%m-%dT%H:%M:%S.%f")
Expand All @@ -305,15 +305,15 @@ def test_update_coin(test_client, test_database):
assert modified_truncated == current_datetime_truncated

# Normal case, partial coin update
coin = {"ruler":"Test Ruler 4", "metal":"Silver", "year":90}
coin = {"name":"Test name 4", "metal":"Silver", "year":90}
test_id = "this-is-a-test-id-0001"
response = test_client.put(f"/v1/coins/id/{test_id}", json=coin)
assert response.status_code == 200
assert response.json()["message"] == "Coin updated successfully"
# Verify coin updated in test database
response = test_client.get(f"/v1/coins/id/{test_id}")
assert response.status_code == 200
assert response.json()["ruler"] == "Test Ruler 4"
assert response.json()["name"] == "Test name 4"
assert "era" not in response.json().keys()
assert "mass" not in response.json().keys()
assert "diameter" not in response.json().keys()
Expand All @@ -323,21 +323,21 @@ def test_update_coin(test_client, test_database):
assert modified_truncated == current_datetime_truncated

# Case with invalid data
coin = {"ruler":"Test Ruler 4", "catalog":5, "metal":"Aluminum", "mass":"heavy"}
coin = {"name":"Test name 4", "catalog":5, "metal":"Aluminum", "mass":"heavy"}
test_id = "this-is-a-test-id-0002"
response = test_client.put(f"/v1/coins/id/{test_id}", json=coin)
assert response.status_code == 422

# Case with missing ID
coin = {"ruler":"Test Ruler 2", "catalog":"Test Catalog", "metal":"Gold"}
coin = {"name":"Test name 2", "catalog":"Test Catalog", "metal":"Gold"}
response = test_client.put("/v1/coins/id/", json=coin)
assert response.status_code == 404

# Partial coin update endpoint
def test_patch_coin(test_client, test_database):

# Normal case, full coin update
coin = {"ruler":"Test Ruler 5", "ruler_detail":"Different Ruler Detail",
coin = {"name":"Test name 5", "name_detail":"Different name Detail",
"catalog":"Catalog 17",
"description":"This is a another new test description, 50 char min.",
"metal":"Copper", "mass":1.5, "diameter":10.0, "era":"AD", "year":117,
Expand All @@ -349,7 +349,7 @@ def test_patch_coin(test_client, test_database):
# Verify coin updated in test database
response = test_client.get(f"/v1/coins/id/{test_id}")
assert response.status_code == 200
assert response.json()["ruler"] == "Test Ruler 5"
assert response.json()["name"] == "Test name 5"
assert response.json()["mass"] == 1.5
assert response.json()["era"] == "AD"
assert len(response.json()) == 14
Expand All @@ -359,15 +359,15 @@ def test_patch_coin(test_client, test_database):
assert modified_truncated == current_datetime_truncated

# Normal case, partial coin update
coin = {"ruler":"Test Ruler 6", "mass":2.1, "diameter":9.1, "txt":"new_txt.txt"}
coin = {"name":"Test name 6", "mass":2.1, "diameter":9.1, "txt":"new_txt.txt"}
test_id = "this-is-a-test-id-0002"
response = test_client.patch(f"/v1/coins/id/{test_id}", json=coin)
assert response.status_code == 200
assert response.json()["message"] == "Coin updated successfully"
# Verify coin updated in test database
response = test_client.get(f"/v1/coins/id/{test_id}")
assert response.status_code == 200
assert response.json()["ruler"] == "Test Ruler 6"
assert response.json()["name"] == "Test name 6"
assert response.json()["mass"] == 2.1
assert response.json()["era"] == "BC"
assert response.json()["year"] == -40
Expand All @@ -379,12 +379,12 @@ def test_patch_coin(test_client, test_database):
assert modified_truncated == current_datetime_truncated

# Case with invalid data
coin = {"ruler":"Test Ruler 7", "catalog":5, "metal":"Aluminum", "mass":"heavy"}
coin = {"name":"Test name 7", "catalog":5, "metal":"Aluminum", "mass":"heavy"}
test_id = "this-is-a-test-id-0001"
response = test_client.patch(f"/v1/coins/id/{test_id}", json=coin)
assert response.status_code == 422

# Case with missing ID
coin = {"ruler":"Test Ruler 2", "catalog":"Test Catalog", "metal":"Gold"}
coin = {"name":"Test name 2", "catalog":"Test Catalog", "metal":"Gold"}
response = test_client.patch("/v1/coins/id/", json=coin)
assert response.status_code == 404
Loading

0 comments on commit afa4470

Please sign in to comment.