Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add GET endpoint for reading room information #302

Merged
merged 28 commits into from
Aug 14, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
8ff7b9d
ulsdevteam#8: Localize Aeon form defaults
ctgraham Sep 2, 2022
4de1900
#241: Production-ready docker configuration
ctgraham Sep 2, 2022
4ef3df3
#241: Production-ready port and doc cleanup
ctgraham Sep 2, 2022
604023c
#241: Production-ready doc cleanup
ctgraham Sep 2, 2022
cff890e
Update python reqs for Django
ctgraham Dec 15, 2022
e1dabca
endpoint to retrieve reading room times from aeon
chryslovelace Feb 3, 2023
ae6f967
wrap data in response object
chryslovelace Feb 10, 2023
6c69348
include closures in reading room data
chryslovelace Feb 10, 2023
783ff13
fix type errors
chryslovelace Feb 10, 2023
ced90db
cache aeon results for a configurable duration
chryslovelace Feb 27, 2023
62a0459
generated migration
chryslovelace Mar 13, 2023
e6e1aed
adding prod entrypoint for db migrations
Mar 23, 2023
bdca24a
fix datetime comparison and serialization bugs
Mar 23, 2023
6e92cda
refresh cache in a background thread
Mar 23, 2023
28e1f82
cron job to refresh reading room cache
chryslovelace Apr 13, 2023
9c76649
cron fixes
Apr 14, 2023
07353a9
update requirements.txt
chryslovelace Apr 14, 2023
79ddea0
Revert "ulsdevteam#8: Localize Aeon form defaults"
chryslovelace Jun 7, 2023
4fc9b4a
use env var for sql port
chryslovelace Jun 7, 2023
d1b19de
Merge branch 'development' into readingroom-endpoint
chryslovelace Jul 31, 2023
290444e
fix import
helrond Aug 7, 2023
68d725b
linting
helrond Aug 7, 2023
b4d5e1e
updating requirements
helrond Aug 7, 2023
b831181
resolving merge conflicts
helrond Aug 7, 2023
f5d2e23
updating vcrpy
helrond Aug 7, 2023
4dce184
make config values optional
helrond Aug 7, 2023
0855187
move collectstatic to entrypoint
helrond Aug 14, 2023
f5fd951
Merge pull request #299 from RockefellerArchiveCenter/readingroom-end…
helrond Aug 14, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions Dockerfile.prod
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
FROM python:3.10

ENV PYTHONUNBUFFERED 1
ARG REQUEST_BROKER_DNS
ARG APPLICATION_PORT
RUN apt-get update
RUN apt-get install --yes apache2 apache2-dev
RUN apt-get install --yes postgresql
RUN apt-get -y install python3-pip
RUN apt-get -y install cron
RUN pip install --upgrade pip

RUN wget https://github.com/GrahamDumpleton/mod_wsgi/archive/refs/tags/4.9.0.tar.gz \
&& tar xvfz 4.9.0.tar.gz \
&& cd mod_wsgi-4.9.0 \
&& ./configure --with-apxs=/usr/bin/apxs --with-python=/usr/local/bin/python \
&& make \
&& make install \
&& make clean

ADD ./apache/000-request_broker.conf /etc/apache2/sites-available/000-request_broker.conf
RUN sed "s/ENV_REQUEST_BROKER_DNS/${REQUEST_BROKER_DNS}/" -i /etc/apache2/sites-available/000-request_broker.conf
RUN sed "s/ENV_REQUEST_BROKER_PORT/${APPLICATION_PORT}/" -i /etc/apache2/sites-available/000-request_broker.conf
ADD ./apache/wsgi.load /etc/apache2/mods-available/wsgi.load
RUN a2ensite 000-request_broker.conf
RUN a2enmod headers
RUN a2enmod rewrite
RUN a2enmod wsgi

COPY . /var/www/request-broker
WORKDIR /var/www/request-broker
RUN pip install -r requirements.txt

COPY --chmod=644 crontab /etc/cron.d/crontab
RUN crontab /etc/cron.d/crontab

EXPOSE ${APPLICATION_PORT}

ENTRYPOINT ["./entrypoint.prod.sh"]
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ Or, if you want to remove all data

The request broker manages configuration by setting environment variables. These variables can be seen in `docker-compose.yml`.

Deployment using the `docker-compose.prod.yml` or `docker-compose.dev.yml` files requires the presence of an `.env.prod` or `.env.dev` file in the root directory of the application. The environment variables included in those files should match the variables in `docker-compose.yml`, although the values assigned to those variables may change.
Deployment using the `Dockerfile.prod` file is intended to bring up a production image (based on Apache/WSGI) which is ready to be proxied publicly by an apache, nginx, traefik or similar frontend. `Dockerfile.prod` expects two environment arguments to be available at build time: `REQUEST_BROKER_DNS` and `APPLICATION_PORT`. Apache will Listen on `${APPLICATION_PORT}` with a ServerName of `${REQUEST_BROKER_DNS}`.

## Services

Expand Down
19 changes: 19 additions & 0 deletions apache/000-request_broker.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
Listen ENV_REQUEST_BROKER_PORT
<VirtualHost *:ENV_REQUEST_BROKER_PORT>
ErrorLog /var/log/apache2/request_broker_error_log
CustomLog /var/log/apache2/request_broker_access_log combined
ServerName ENV_REQUEST_BROKER_DNS
DocumentRoot /var/www/html/
Alias /static /var/www/request-broker/static
<Directory /var/www/request-broker/static>
Require all granted
</Directory>
<Directory /var/www/request-broker/request_broker>
WSGIProcessGroup request_broker
WSGIApplicationGroup %{GLOBAL}
Require all granted
</Directory>
WSGIDaemonProcess request_broker home=/var/www/request-broker
WSGIProcessGroup request_broker
WSGIScriptAlias / /var/www/request-broker/request_broker/wsgi.py
</VirtualHost>
1 change: 1 addition & 0 deletions apache/wsgi.load
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
LoadModule wsgi_module /usr/lib/apache2/modules/mod_wsgi.so
2 changes: 2 additions & 0 deletions crontab
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
*/5 * * * * /usr/local/bin/python3 -u /var/www/request-broker/manage.py runcrons >/proc/1/fd/1 2>/proc/1/fd/2

16 changes: 16 additions & 0 deletions entrypoint.prod.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
#!/bin/bash

./wait-for-it.sh db:${SQL_PORT} -- echo "Apply database migrations"
python manage.py migrate

# Collect static files
echo "Collecting static files"
python manage.py collectstatic

#Start cron
echo "Starting cron"
cron

#Start server
echo "Starting server"
apache2ctl -D FOREGROUND
2 changes: 1 addition & 1 deletion entrypoint.sh
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ if [ ! -f request_broker/config.py ]; then
cp request_broker/config.py.example request_broker/config.py
fi

./wait-for-it.sh db:5432 -- echo "Apply database migrations"
./wait-for-it.sh db:${SQL_PORT} -- echo "Apply database migrations"
python manage.py migrate

#Start server
Expand Down
13 changes: 8 additions & 5 deletions process_request/clients.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
from requests import Session

from request_broker import settings


def http_meth_factory(meth):
"""Utility method for producing HTTP proxy methods.
Expand All @@ -27,10 +25,15 @@ def __init__(cls, name, parents, dct):

class AeonAPIClient(metaclass=ProxyMethods):

def __init__(self, baseurl):
def __init__(self, baseurl, apikey):
self.baseurl = baseurl
self.session = Session()
self.session.headers.update(
{"Accept": "application/json",
"User-Agent": "AeonAPIClient/0.1",
"X-AEON-API-KEY": settings.AEON_API_KEY})
"X-AEON-API-KEY": apikey})

def get_reading_rooms(self):
return self.get("ReadingRooms").json()

def get_closures(self, reading_room_id):
return self.get("/".join(["ReadingRooms", str(reading_room_id), "Closures"])).json()
15 changes: 15 additions & 0 deletions process_request/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@
from django.conf import settings
from ordered_set import OrderedSet

from .clients import AeonAPIClient
from .models import ReadingRoomCache

CONFIDENCE_RATIO = 97 # Minimum confidence ratio to match against.
OPEN_TEXT = ["Open for research", "Open for scholarly research"]
CLOSED_TEXT = ["Restricted"]
Expand Down Expand Up @@ -437,3 +440,15 @@ def get_formatted_resource_id(resource, client):
Concatenates the resource id parts using the separator from the config
"""
return format_resource_id(resource, client, settings.RESOURCE_ID_SEPARATOR)


def refresh_reading_room_cache():
"""Gets reading room data from the Aeon API and stores it in the database as json

Returns the newly saved object
"""
aeon = AeonAPIClient(baseurl=settings.AEON["baseurl"], apikey=settings.AEON["apikey"])
reading_rooms = aeon.get_reading_rooms()
for reading_room in reading_rooms:
reading_room["closures"] = aeon.get_closures(reading_room["id"])
return ReadingRoomCache.objects.update_or_create(defaults={'json': json.dumps(reading_rooms)})[0]
21 changes: 21 additions & 0 deletions process_request/migrations/0004_readingroomcache.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Generated by Django 4.0.6 on 2023-03-13 17:55

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('process_request', '0003_auto_20211106_1625'),
]

operations = [
migrations.CreateModel(
name='ReadingRoomCache',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('timestamp', models.DateTimeField(auto_now=True)),
('json', models.TextField()),
],
),
]
6 changes: 6 additions & 0 deletions process_request/models.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from django.contrib.auth.models import AbstractUser
from django.db import models


class User(AbstractUser):
Expand All @@ -25,3 +26,8 @@ def __str__(self):
Returns the full name and email of a user.
"""
return '{} <{}>'.format(self.full_name, self.email)


class ReadingRoomCache(models.Model):
timestamp = models.DateTimeField(auto_now=True)
json = models.TextField()
27 changes: 24 additions & 3 deletions process_request/views.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
import csv
from datetime import datetime
import threading
from datetime import datetime, timedelta

from asnake.aspace import ASpace
from django.http import StreamingHttpResponse
from django.http import HttpResponse, StreamingHttpResponse
from django.shortcuts import redirect
from django.utils import timezone
from rest_framework.response import Response
from rest_framework.views import APIView

from request_broker import settings

from .helpers import resolve_ref_id
from .helpers import refresh_reading_room_cache, resolve_ref_id
from .models import ReadingRoomCache
from .routines import AeonRequester, Mailer, Processor


Expand Down Expand Up @@ -139,6 +142,24 @@ def get(self, request):
return Response({"detail": str(e)}, status=500)


class AeonReadingRoomsView(APIView):
"""Returns reading room information from Aeon"""

def get(self, request):
try:
try:
cached_reading_rooms = ReadingRoomCache.objects.get()
cache_needs_refresh = cached_reading_rooms.timestamp + timedelta(minutes=settings.AEON["cache_duration"]) < timezone.now()
except ReadingRoomCache.DoesNotExist:
cached_reading_rooms = refresh_reading_room_cache()
cache_needs_refresh = False
if cache_needs_refresh:
threading.Thread(target=refresh_reading_room_cache).start()
return HttpResponse(cached_reading_rooms.json, content_type='application/json')
except Exception as e:
return Response({"detail": str(e)}, status=500)


class PingView(APIView):
"""Checks if the application is able to process requests."""

Expand Down
3 changes: 3 additions & 0 deletions request_broker/config.py.deploy
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,7 @@ DIMES_BASEURL = "${DIMES_BASEURL}"
RESTRICTED_IN_CONTAINER = ${RESTRICTED_IN_CONTAINER}
OFFSITE_BUILDINGS = ${OFFSITE_BUILDINGS}
RESOURCE_ID_SEPARATOR = "${RESOURCE_ID_SEPARATOR}"
AEON_BASEURL = "${AEON_BASEURL}"
AEON_APIKEY = "${AEON_APIKEY}"
AEON_CACHE_DURATION = ${AEON_CACHE_DURATION}
USE_LOCATION_TITLE = ${USE_LOCATION_TITLE}
3 changes: 3 additions & 0 deletions request_broker/config.py.example
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,7 @@ DIMES_BASEURL = "https://dimes.rockarch.org" # Base URL for DIMES application
RESTRICTED_IN_CONTAINER = False # Fetch a list of restricted items in the same container as the requested item.
OFFSITE_BUILDINGS = ["Armonk", "Greenrock"] # Names of offsite buildings, which will be added to locations (list of strings)
RESOURCE_ID_SEPARATOR = ':'
AEON_BASEURL = "https://servername/aeon/api/" # base URL for an Aeon API instance
AEON_APIKEY = "00000000-0000-0000-0000-000000000000" # API key for the Aeon API
AEON_CACHE_DURATION = 7200 # cache duration in minutes
USE_LOCATION_TITLE = False # Use the title field from a top container location
12 changes: 12 additions & 0 deletions request_broker/cron.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
from django_cron import CronJobBase, Schedule

from process_request.helpers import refresh_reading_room_cache
from request_broker.settings import AEON


class RefreshReadingRoomCache(CronJobBase):
schedule = Schedule(run_every_mins=AEON["cache_duration"])
code = 'request_broker.refresh_reading_room_cache'

def do(self):
refresh_reading_room_cache()
11 changes: 11 additions & 0 deletions request_broker/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'django_cron',
'corsheaders',
'process_request',
'rest_framework',
Expand Down Expand Up @@ -171,3 +172,13 @@
OFFSITE_BUILDINGS = getattr(config, 'OFFSITE_BUILDINGS', [])
USE_LOCATION_TITLE = config.USE_LOCATION_TITLE
RESOURCE_ID_SEPARATOR = config.RESOURCE_ID_SEPARATOR

AEON = {
"baseurl": getattr(config, 'AEON_BASEURL', ''),
"apikey": getattr(config, 'AEON_APIKEY', ''),
"cache_duration": getattr(config, 'AEON_CACHE_DURATION', 7200),
}

CRON_CLASSES = [
"request_broker.cron.RefreshReadingRoomCache"
]
4 changes: 3 additions & 1 deletion request_broker/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@
from django.contrib import admin
from django.urls import path

from process_request.views import (DeliverDuplicationRequestView,
from process_request.views import (AeonReadingRoomsView,
DeliverDuplicationRequestView,
DeliverReadingRoomRequestView,
DownloadCSVView, LinkResolverView,
MailerView, ParseBatchRequestView,
Expand All @@ -31,5 +32,6 @@
path("api/process-request/parse-batch", ParseBatchRequestView.as_view(), name="parse-batch"),
path("api/process-request/resolve", LinkResolverView.as_view(), name="resolve-request"),
path("api/download-csv/", DownloadCSVView.as_view(), name="download-csv"),
path("api/reading-rooms/", AeonReadingRoomsView.as_view(), name="get-readingrooms"),
path("api/status/", PingView.as_view(), name="ping")
]
9 changes: 5 additions & 4 deletions requirements.in
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
ArchivesSnake~=0.9
Django~=4.1
django-cors-headers~=3.12
django-cors-headers~=3.13
django-csp~=3.7
django4-cron~=0.5
djangorestframework~=3.13
inflect~=5.6
inflect~=6.0.0
ordered-set~=4.1
psycopg2~=2.9
requests~=2.28
shortuuid~=1.0
vcrpy~=4.2
shortuuid~=1.0.9
vcrpy~=5.1
18 changes: 13 additions & 5 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -19,20 +19,26 @@ charset-normalizer==3.2.0
django==4.2.3
# via
# -r requirements.in
# django-common-helpers
# django-cors-headers
# django-csp
# django4-cron
# djangorestframework
django-common-helpers==0.9.2
# via django4-cron
django-cors-headers==3.14.0
# via -r requirements.in
django-csp==3.7
# via -r requirements.in
django4-cron==0.5.1
# via -r requirements.in
djangorestframework==3.14.0
# via -r requirements.in
idna==3.4
# via
# requests
# yarl
inflect==5.6.2
inflect==6.0.5
# via -r requirements.in
more-itertools==10.0.0
# via archivessnake
Expand All @@ -42,6 +48,8 @@ ordered-set==4.1.0
# via -r requirements.in
psycopg2==2.9.6
# via -r requirements.in
pydantic==1.10.12
# via inflect
pytz==2023.3
# via djangorestframework
pyyaml==6.0.1
Expand All @@ -56,17 +64,17 @@ requests==2.31.0
# archivessnake
shortuuid==1.0.11
# via -r requirements.in
six==1.16.0
# via vcrpy
sqlparse==0.4.4
# via django
structlog==23.1.0
# via archivessnake
typing-extensions==4.7.1
# via asgiref
# via
# asgiref
# pydantic
urllib3==2.0.4
# via requests
vcrpy==4.4.0
vcrpy==5.1.0
# via -r requirements.in
wrapt==1.15.0
# via vcrpy
Expand Down