Skip to content

Commit

Permalink
Initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
ahosgood committed Sep 12, 2024
1 parent 719bb07 commit b98e2e0
Show file tree
Hide file tree
Showing 12 changed files with 225 additions and 70 deletions.
3 changes: 2 additions & 1 deletion .dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@
docker-compose.yml
Dockerfile
README.md
.gitignore
.gitignore
.env
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
.DS_Store
__pycache__
.pytest_cache
.pytest_cache
.env
16 changes: 10 additions & 6 deletions app/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,24 @@ def create_app(config_class):
config = get_config(config_class)

app = FastAPI(
title="TNA FastAPI Application", log_level=config.get("LOG_LEVEL")
title="The National Archives opening times",
log_level=config.get("LOG_LEVEL"),
)
app.state.config = {"BASE_URI": config.get("BASE_URI")}
app.state.config = {
"BASE_URI": config.get("BASE_URI"),
"GOOGLE_MAPS_API_KEY": config.get("GOOGLE_MAPS_API_KEY"),
"GOOGLE_MAPS_PLACE_ID": config.get("GOOGLE_MAPS_PLACE_ID"),
}
if config.get("FORCE_HTTPS"):
app.add_middleware(HTTPSRedirectMiddleware)

from .greetings import routes as greetings_routes
from .healthcheck import routes as healthcheck_routes
from .main import routes as main_routes

app.include_router(healthcheck_routes.router, prefix="/healthcheck")
app.include_router(
greetings_routes.router,
prefix=f"{config.get('BASE_URI')}/greetings",
tags=["Examples"],
main_routes.router,
prefix=config.get("BASE_URI"),
)

return app
26 changes: 0 additions & 26 deletions app/greetings/routes.py

This file was deleted.

2 changes: 1 addition & 1 deletion app/greetings/__init__.py → app/main/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@

router = APIRouter()

from app.greetings import routes # noqa: E402,F401
from app.main import routes # noqa: E402,F401
135 changes: 135 additions & 0 deletions app/main/routes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
import datetime

import requests
from app.main import router
from fastapi import Request
from pydantic import BaseModel
from pydash import objects


class ClosingTime(BaseModel):
close: str


class OpeningTimes(ClosingTime):
open: str


class OpeningTimesDay(OpeningTimes):
day: str
day_alt: str

def __init__(self, open: str, close: str, day_number: int):
days = [
"Sunday",
"Monday",
"Tuesday",
"Wednesday",
"Thursday",
"Friday",
"Saturday",
]
today_dow = (datetime.datetime.today().weekday() + 1) % 7
day_alt = ""
if today_dow == day_number:
day_alt = "today"
elif (today_dow + 1) % 7 == day_number:
day_alt = "tomorrow"
super().__init__(
open=open, close=close, day=days[day_number], day_alt=day_alt
)


@router.get("/is-open-now/")
async def is_open_now(
request: Request,
) -> bool:
config = request.app.state.config
url = f"https://maps.googleapis.com/maps/api/place/details/json?fields=current_opening_hours%2Copening_hours%2Csecondary_opening_hours&place_id={config['GOOGLE_MAPS_PLACE_ID']}&key={config['GOOGLE_MAPS_API_KEY']}"
response = requests.get(url)
if response.status_code == 404:
raise Exception("Resource not found")
if response.status_code != requests.codes.ok:
raise ConnectionError("Request to API failed")
try:
result = response.json()
except ValueError:
raise ConnectionError("Cannot parse JSON")
return objects.get(result, "result.current_opening_hours.open_now") or False


@router.get("/today/")
async def today(
request: Request,
) -> OpeningTimes:
config = request.app.state.config
url = f"https://maps.googleapis.com/maps/api/place/details/json?fields=current_opening_hours%2Copening_hours%2Csecondary_opening_hours&place_id={config['GOOGLE_MAPS_PLACE_ID']}&key={config['GOOGLE_MAPS_API_KEY']}"
response = requests.get(url)
if response.status_code == 404:
raise Exception("Resource not found")
if response.status_code != requests.codes.ok:
raise ConnectionError("Request to API failed")
try:
result = response.json()
except ValueError:
raise ConnectionError("Cannot parse JSON")
today_dow = (datetime.datetime.today().weekday() + 1) % 7
if current_opening_hours := objects.get(
result, "result.current_opening_hours"
):
if periods := objects.get(current_opening_hours, "periods"):
today_opening_times = (
next(
period
for period in periods
if period["open"]["day"] == today_dow
)
or periods[0]
)
return OpeningTimes(
open=today_opening_times["open"]["time"],
close=today_opening_times["close"]["time"],
)
raise ConnectionError("Cannot parse data")


@router.get("/next-open/")
async def next_open(
request: Request,
) -> OpeningTimesDay:
config = request.app.state.config
url = f"https://maps.googleapis.com/maps/api/place/details/json?fields=current_opening_hours%2Copening_hours%2Csecondary_opening_hours&place_id={config['GOOGLE_MAPS_PLACE_ID']}&key={config['GOOGLE_MAPS_API_KEY']}"
response = requests.get(url)
if response.status_code == 404:
raise Exception("Resource not found")
if response.status_code != requests.codes.ok:
raise ConnectionError("Request to API failed")
try:
result = response.json()
except ValueError:
raise ConnectionError("Cannot parse JSON")
print(result)
if current_opening_hours := objects.get(
result, "result.current_opening_hours"
):
if periods := objects.get(current_opening_hours, "periods"):
today_dow = (datetime.datetime.today().weekday() + 1) % 7
now = datetime.datetime.now().strftime("%H%M")
next_opening_times = (
next(
period
for period in periods
if period["open"]["day"] > today_dow
or (
period["open"]["day"] == today_dow
and int(period["open"]["time"]) > int(now)
)
)
or periods[0]
)
return OpeningTimesDay(
open=next_opening_times["open"]["time"],
close=next_opening_times["close"]["time"],
day_number=next_opening_times["open"]["day"],
)
raise ConnectionError("Cannot parse data")
3 changes: 3 additions & 0 deletions config.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ class Base(object):

FORCE_HTTPS: bool = strtobool(os.getenv("FORCE_HTTPS", "True"))

GOOGLE_MAPS_API_KEY: str = os.getenv("GOOGLE_MAPS_API_KEY", "")
GOOGLE_MAPS_PLACE_ID: str = os.getenv("GOOGLE_MAPS_PLACE_ID", "")


class Production(Base, Features):
pass
Expand Down
5 changes: 5 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,21 @@ services:
args:
IMAGE: ghcr.io/nationalarchives/tna-python
IMAGE_TAG: preview
env_file: .env
environment:
- ENVIRONMENT=develop
- CONFIG=config.Develop
- SECRET_KEY=abc123
- GOOGLE_MAPS_PLACE_ID=ChIJYe6r5ngOdkgRf6SVcbMsQ0k
ports:
- 65531:8080
volumes:
- ./:/app
dev:
image: ghcr.io/nationalarchives/tna-python-dev:preview
env_file: .env
environment:
- GOOGLE_MAPS_PLACE_ID=ChIJYe6r5ngOdkgRf6SVcbMsQ0k
volumes:
- ./:/app

Expand Down
19 changes: 18 additions & 1 deletion poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[tool.poetry]
name = "tna-fastapi-application"
name = "ds-opening-times-api"
version = "0.1.0"
description = ""
authors = ["Andrew Hosgood <andrew.hosgood@nationalarchives.gov.uk>"]
Expand All @@ -10,6 +10,7 @@ packages = [{include = "app"}]
python = "^3.12"
requests = "^2.31.0"
fastapi = "^0.112.0"
pydash = "^8.0.3"

[tool.poetry.group.dev]
optional = true
Expand Down
33 changes: 0 additions & 33 deletions test/test_greetings.py

This file was deleted.

47 changes: 47 additions & 0 deletions test/test_main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import json
import unittest

from fastapi.testclient import TestClient

from app import create_app


class MainBlueprintTestCase(unittest.TestCase):
def setUp(self):
self.app = create_app("config.Test")
self.client = TestClient(self.app)
self.domain = "http://localhost"

def test_is_open_now(self):
rv = self.client.get(
f"{self.app.state.config.get('BASE_URI')}/is-open-now/",
)
self.assertEqual(rv.status_code, 200)
response_json = json.loads(rv.text)
self.assertIsInstance(response_json, bool)

def test_today(self):
rv = self.client.get(
f"{self.app.state.config.get('BASE_URI')}/today/",
)
self.assertEqual(rv.status_code, 200)
response_json = json.loads(rv.text)
self.assertIn("open", response_json)
self.assertIsInstance(response_json["open"], str)
self.assertIn("close", response_json)
self.assertIsInstance(response_json["close"], str)

def test_next_open(self):
rv = self.client.get(
f"{self.app.state.config.get('BASE_URI')}/next-open/",
)
self.assertEqual(rv.status_code, 200)
response_json = json.loads(rv.text)
self.assertIn("open", response_json)
self.assertIsInstance(response_json["open"], str)
self.assertIn("close", response_json)
self.assertIsInstance(response_json["close"], str)
self.assertIn("day", response_json)
self.assertIsInstance(response_json["day"], str)
self.assertIn("day_alt", response_json)
self.assertIsInstance(response_json["day_alt"], str)

0 comments on commit b98e2e0

Please sign in to comment.