-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
3f75b2a
commit 75863a2
Showing
19 changed files
with
568 additions
and
433 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
from typing import Final | ||
|
||
from django.utils import timezone | ||
|
||
BATCH_NAME_DEFAULT: Final[str] = f"Batch {timezone.now()}" |
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
from django.apps import AppConfig | ||
|
||
|
||
class Config(AppConfig): | ||
name = __name__.rpartition(".")[0] | ||
verbose_name = "Country Workspace | Aurora" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,64 @@ | ||
from typing import Any, Generator | ||
from urllib.parse import urljoin | ||
|
||
import requests | ||
from constance import config | ||
|
||
|
||
class AuroraClient: | ||
""" | ||
A client for interacting with the Aurora API. | ||
Provides methods to fetch data from the Aurora API with authentication. | ||
Handles pagination automatically for large datasets. | ||
""" | ||
|
||
def __init__(self, token: str | None = None) -> None: | ||
""" | ||
Initialize the AuroraClient. | ||
Args: | ||
token (str | None): An optional API token for authentication. If not provided, | ||
the token is retrieved from the Constance configuration (config.AURORA_API_TOKEN). | ||
""" | ||
self.token = token or config.AURORA_API_TOKEN | ||
|
||
def _get_url(self, path: str) -> str: | ||
""" | ||
Construct a fully qualified URL for the Aurora API. | ||
Args: | ||
path (str): The relative API path. | ||
Returns: | ||
str: The full URL, ensuring it ends with a trailing slash. | ||
""" | ||
url = urljoin(config.AURORA_API_URL, path) | ||
if not url.endswith("/"): | ||
url = url + "/" | ||
return url | ||
|
||
def get(self, path: str) -> Generator[dict[str, Any], None, None]: | ||
""" | ||
Fetch records from the Aurora API with automatic pagination. | ||
Args: | ||
path (str): The relative API path to fetch data from. | ||
Yields: | ||
dict[str, Any]: Individual records from the API. | ||
Raises: | ||
Exception: If the API response has a status code other than 200. | ||
""" | ||
url = self._get_url(path) | ||
while url: | ||
ret = requests.get(url, headers={"Authorization": f"Token {self.token}"}, timeout=10) | ||
if ret.status_code != 200: | ||
raise Exception(f"Error {ret.status_code} fetching {url}") | ||
data = ret.json() | ||
|
||
for record in data["results"]: | ||
yield record | ||
|
||
url = data.get("next") |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
from django import forms | ||
|
||
|
||
class ImportAuroraForm(forms.Form): | ||
|
||
batch_name = forms.CharField(required=False, help_text="Label for this batch") | ||
|
||
check_before = forms.BooleanField(required=False, help_text="Prevent import if errors") | ||
|
||
household_name_column = forms.CharField( | ||
required=False, | ||
initial="family_name", | ||
help_text="Which Individual's column contains the Household's name", | ||
) | ||
|
||
fail_if_alien = forms.BooleanField(required=False) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,125 @@ | ||
from typing import Any | ||
|
||
from django.db.transaction import atomic | ||
|
||
from country_workspace.constants import BATCH_NAME_DEFAULT | ||
from country_workspace.contrib.aurora.client import AuroraClient | ||
from country_workspace.models import AsyncJob, Batch, Household, Individual, User | ||
from country_workspace.utils.fields import clean_field_name | ||
|
||
|
||
def sync_aurora_job(job: AsyncJob) -> dict[str, Any]: | ||
""" | ||
Synchronizes data from the Aurora system into the database for the given job within an atomic transaction. | ||
Args: | ||
job (AsyncJob): The job instance containing configuration and context for synchronization. | ||
Returns: | ||
dict[str, Any]: A dictionary with counts of households and individuals created, e.g., | ||
{"households": total_hh, "individuals": total_ind}. | ||
""" | ||
total_hh = total_ind = 0 | ||
client = AuroraClient() | ||
with atomic(): | ||
job.batch = _create_batch(job) | ||
job.save() | ||
# TODO: Duplicate import ? | ||
for record in client.get("record"): | ||
for f_name, f_value in record["fields"].items(): | ||
if f_name == "household": | ||
hh = _create_household(job, f_value[0]) | ||
total_hh += 1 | ||
elif f_name == "individuals": | ||
total_ind += len(_create_individuals(job, hh, f_value)) | ||
return {"households": total_hh, "individuals": total_ind} | ||
|
||
|
||
def _create_batch(job: AsyncJob) -> Batch: | ||
""" | ||
Creates a batch entity associated with the given job. | ||
Args: | ||
job (AsyncJob): The job instance containing the configuration for the batch creation. | ||
Returns: | ||
Batch: The newly created batch instance. | ||
""" | ||
return Batch.objects.create( | ||
name=job.config.get("batch_name") or BATCH_NAME_DEFAULT, | ||
program=job.program, | ||
country_office=job.program.country_office, | ||
imported_by=User.objects.get(pk=job.config.get("imported_by_id")), | ||
) | ||
|
||
|
||
def _create_household(job: AsyncJob, fields: dict[str, Any]) -> Household: | ||
""" | ||
Creates a household entity associated with the given job and batch. | ||
Args: | ||
job (AsyncJob): The job instance containing context for household creation. | ||
fields (dict[str, Any]): A dictionary containing household data fields. | ||
Returns: | ||
Household: The newly created household instance. | ||
""" | ||
return job.program.households.create( | ||
batch=job.batch, flex_fields={clean_field_name(k): v for k, v in fields.items()} | ||
) | ||
|
||
|
||
def _create_individuals( | ||
job: AsyncJob, | ||
household: Household, | ||
fields: list[dict[str, Any]], | ||
) -> list[Individual]: | ||
""" | ||
Creates individuals associated with a household and updates the household name if necessary. | ||
Args: | ||
job (AsyncJob): The job instance containing configuration and context for individual creation. | ||
household (Household): The household to associate with the individuals. | ||
fields (list[dict[str, Any]]): A list of dictionaries containing individual data fields. | ||
Returns: | ||
list[Individual]: The list of newly created individual instances. | ||
""" | ||
individuals = [] | ||
for individual in fields: | ||
|
||
_update_household_name_from_individual(job, household, individual) | ||
|
||
fullname = next((k for k in individual if k.startswith("given_name")), None) | ||
individuals.append( | ||
Individual( | ||
batch=job.batch, | ||
household_id=household.pk, | ||
name=individual.get(fullname, ""), | ||
flex_fields={clean_field_name(k): v for k, v in individual.items()}, | ||
) | ||
) | ||
|
||
return job.program.individuals.bulk_create(individuals) | ||
|
||
|
||
def _update_household_name_from_individual(job: AsyncJob, household: Household, individual: dict[str, Any]) -> None: | ||
""" | ||
Updates the household name based on an individual's relationship and name field. | ||
This method checks if the individual is marked as the head of the household | ||
and updates the household name accordingly. | ||
Args: | ||
job (AsyncJob): The job instance containing configuration details. | ||
household (Household): The household to update. | ||
individual (dict[str, Any]): The individual data containing potential household name information. | ||
Returns: | ||
None | ||
""" | ||
if any(individual.get(k) == "head" for k in individual if k.startswith("relationship")): | ||
for k, v in individual.items(): | ||
if clean_field_name(k) == job.config["household_name_column"]: | ||
job.program.households.filter(pk=household.pk).update(name=v) | ||
break |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.