Skip to content

Commit a9ba507

Browse files
committed
Add ingest script for country plan public file
1 parent 40af6ae commit a9ba507

File tree

8 files changed

+192
-1
lines changed

8 files changed

+192
-1
lines changed

api/management/commands/ingest_appeals.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ def get_new_or_modified_appeals(self):
8787
# get latest BILATERALS
8888
logger.info('Querying appeals API for new appeals data (bilateral)')
8989
url = 'http://go-api.ifrc.org/api/appealbilaterals'
90-
auth = (os.getenv('APPEALS_USER'), os.getenv('APPEALS_PASS'))
90+
auth = (settings.APPEALS_USER, settings.APPEALS_PASS)
9191

9292
adapter = HTTPAdapter(max_retries=settings.RETRY_STRATEGY)
9393
sess = Session()

api/models.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1925,6 +1925,7 @@ def __str__(self):
19251925
return '%s | %s | %s' % (self.name, self.get_status_display(), str(self.created_at)[5:16]) # omit irrelevant 0
19261926

19271927
# Given a request containing new CronJob log row, validate and add the CronJob log row.
1928+
@staticmethod
19281929
def sync_cron(body):
19291930
new = []
19301931
errors = []
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
import logging
2+
import requests
3+
from typing import Union
4+
from datetime import datetime
5+
from django.utils.timezone import make_aware
6+
from django.core.management.base import BaseCommand
7+
from django.conf import settings
8+
from django.db import models
9+
10+
from api.models import CronJob, CronJobStatus
11+
from country_plan.models import CountryPlan
12+
13+
logger = logging.getLogger(__name__)
14+
15+
NAME = 'ingest_country_plan_file'
16+
# Ref: https://github.com/IFRCGo/go-api/issues/1614
17+
SOURCE = 'https://go-api.ifrc.org/api/publicsiteappeals?AppealsTypeID=1851&Hidden=false'
18+
19+
20+
class Command(BaseCommand):
21+
@staticmethod
22+
def parse_date(text: str) -> Union[datetime, None]:
23+
"""
24+
Convert Appeal API datetime into django datetime
25+
Parameters
26+
----------
27+
text : str
28+
Datetime eg: 2022-11-29T11:24:00
29+
"""
30+
if text:
31+
return make_aware(
32+
# NOTE: Format is assumed by looking at the data from Appeal API
33+
datetime.strptime(text, '%Y-%m-%dT%H:%M:%S')
34+
)
35+
36+
def load_for_country(self, country_data):
37+
country_iso2 = country_data.get('LocationCountryCode')
38+
country_name = country_data.get('LocationCountryName')
39+
public_plan_url = country_data.get('BaseDirectory') or '' + country_data.get('BaseFileName') or ''
40+
inserted_date = self.parse_date(country_data.get('Inserted'))
41+
if (
42+
(country_iso2 is None and country_name is None) or
43+
public_plan_url is None or
44+
inserted_date is None
45+
):
46+
return
47+
country_plan = CountryPlan.objects.filter(
48+
models.Q(country__iso__iexact=country_iso2) |
49+
models.Q(country__name__iexact=country_name)
50+
).first()
51+
if country_plan is None:
52+
logger.warning(f'{NAME} No country_plan found for: {(country_iso2, country_name)}')
53+
return
54+
if country_plan.appeal_api_inserted_date and country_plan.appeal_api_inserted_date >= inserted_date:
55+
# No need to do anything here
56+
return
57+
public_plan_url = country_data['BaseDirectory'] + country_data['BaseFileName']
58+
country_plan.appeal_api_inserted_date = inserted_date
59+
country_plan.load_file_to_country_plan(
60+
public_plan_url,
61+
# NOTE: File provided are PDF,
62+
f"public-plan-{country_data['BaseFileName']}.pdf",
63+
)
64+
country_plan.is_publish = True
65+
country_plan.save(
66+
update_fields=(
67+
'appeal_api_inserted_date',
68+
'public_plan_file', # By load_file_to_country_plan
69+
'is_publish',
70+
)
71+
)
72+
return True
73+
74+
def load(self):
75+
updated = 0
76+
auth = (settings.APPEALS_USER, settings.APPEALS_PASS)
77+
results = requests.get(SOURCE, auth=auth, headers={'Accept': 'application/json'}).json()
78+
for result in results:
79+
try:
80+
if self.load_for_country(result):
81+
updated += 1
82+
except Exception as ex:
83+
logger.error('Could not Updated countries plan', exc_info=True)
84+
country_info = (
85+
result.get('LocationCountryCode'),
86+
result.get('LocationCountryName'),
87+
)
88+
CronJob.sync_cron({
89+
'name': NAME,
90+
'message': f"Could not updated country plan for {country_info}\n\nException:\n{str(ex)}",
91+
'status': CronJobStatus.ERRONEOUS,
92+
})
93+
return updated
94+
95+
def handle(self, *args, **kwargs):
96+
try:
97+
logger.info('\nFetching data for country plans:: ')
98+
countries_plan_updated = self.load()
99+
CronJob.sync_cron({
100+
'name': NAME,
101+
'message': 'Updated countries plan',
102+
'num_result': countries_plan_updated,
103+
'status': CronJobStatus.SUCCESSFUL,
104+
})
105+
logger.info('Updated countries plan')
106+
except Exception as ex:
107+
logger.error('Could not Updated countries plan', exc_info=True)
108+
CronJob.sync_cron({
109+
'name': NAME,
110+
'message': f'Could not Updated countries plan\n\nException:\n{str(ex)}',
111+
'status': CronJobStatus.ERRONEOUS,
112+
})
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# Generated by Django 3.2.16 on 2023-01-02 07:31
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
('country_plan', '0003_auto_20221128_0831'),
10+
]
11+
12+
operations = [
13+
migrations.AddField(
14+
model_name='countryplan',
15+
name='appeal_api_inserted_date',
16+
field=models.DateTimeField(blank=True, null=True),
17+
),
18+
]

country_plan/models.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@
33
from django.utils import timezone
44
from django.utils.translation import gettext_lazy as _
55
from django.core.validators import FileExtensionValidator
6+
from django.core.files.base import File
67

8+
from main.utils import DownloadFileManager
79
from api.models import Country
810

911

@@ -80,9 +82,21 @@ class CountryPlan(CountryPlanAbstract):
8082
people_targeted = models.IntegerField(verbose_name=_('People Targeted'), blank=True, null=True)
8183
is_publish = models.BooleanField(default=False, verbose_name=_('Published'))
8284

85+
# NOTE: Used to sync with Appeal API (ingest_country_plan_file.py for more info)
86+
appeal_api_inserted_date = models.DateTimeField(blank=True, null=True)
87+
8388
def __str__(self):
8489
return f'{self.country}'
8590

91+
def load_file_to_country_plan(self, url: str, filename: str, commit=False):
92+
with DownloadFileManager(url, suffix='.pdf') as f:
93+
self.public_plan_file.save(
94+
filename,
95+
File(f),
96+
)
97+
if commit:
98+
self.save()
99+
86100
# NOTE: To be used by CountryPlanSerializer (From CountryPlanViewset)
87101
def full_country_plan_mc(self):
88102
all_mc = list(self.country_plan_mc.all())

docker-compose.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@ x-server: &base_server_setup
2424
ERP_API_ENDPOINT: ${ERP_API_ENDPOINT:-https://ifrctintapim001.azure-api.net/GoAPI/ExtractGoEmergency}
2525
ERP_API_SUBSCRIPTION_KEY: ${ERP_API_SUBSCRIPTION_KEY:-abcdef}
2626
CELERY_REDIS_URL: ${CELERY_REDIS_URL:-redis://redis:6379/0}
27+
# Appeal API
28+
APPEALS_USER: ${APPEALS_USER}
29+
APPEALS_PASS: ${APPEALS_PASS}
2730
env_file:
2831
- .env
2932
volumes:

main/settings.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,9 @@
6969
GO_FTPUSER=(str, None),
7070
GO_FTPPASS=(str, None),
7171
GO_DBPASS=(str, None),
72+
# Appeal Server Credentials (https://go-api.ifrc.org/api/)
73+
APPEALS_USER=(str, None),
74+
APPEALS_PASS=(str, None),
7275
)
7376

7477

@@ -471,3 +474,7 @@
471474

472475

473476
DREF_OP_UPDATE_FINAL_REPORT_UPDATE_ERROR_MESSAGE = "OBSOLETE_PAYLOAD"
477+
478+
# Appeal Server Credentials
479+
APPEALS_USER = env('APPEALS_USER')
480+
APPEALS_PASS = env('APPEALS_PASS')

main/utils.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
import requests
2+
3+
from tempfile import NamedTemporaryFile, _TemporaryFileWrapper
14
from collections import defaultdict
25

36

@@ -24,3 +27,36 @@ def get_merged_items_by_fields(items, fields, seperator=', '):
2427
field: seperator.join(data[field])
2528
for field in fields
2629
}
30+
31+
32+
class DownloadFileManager():
33+
"""
34+
Convert Appeal API datetime into django datetime
35+
Parameters
36+
----------
37+
url : str
38+
Return: TemporaryFile
39+
On close: Close and Delete the file
40+
"""
41+
def __init__(self, url, dir='/tmp/', **kwargs):
42+
self.url = url
43+
self.downloaded_file = None
44+
# NamedTemporaryFile attributes
45+
self.named_temporary_file_args = {
46+
'dir': dir,
47+
**kwargs,
48+
}
49+
50+
def __enter__(self) -> _TemporaryFileWrapper:
51+
file = NamedTemporaryFile(delete=True, **self.named_temporary_file_args)
52+
with requests.get(self.url, stream=True) as r:
53+
r.raise_for_status()
54+
for chunk in r.iter_content(chunk_size=8192):
55+
file.write(chunk)
56+
file.flush()
57+
self.downloaded_file = file
58+
return self.downloaded_file
59+
60+
def __exit__(self, *_):
61+
if self.downloaded_file:
62+
self.downloaded_file.close()

0 commit comments

Comments
 (0)