|
7 | 7 | import requests |
8 | 8 |
|
9 | 9 | from process_report.invoices import invoice |
10 | | -from process_report.processors import processor |
| 10 | +from process_report.processors import processor, validate_billable_pi_processor |
11 | 11 |
|
12 | 12 | logger = logging.getLogger(__name__) |
13 | 13 | logging.basicConfig(level=logging.INFO) |
14 | 14 |
|
15 | 15 |
|
16 | 16 | CF_ATTR_ALLOCATED_PROJECT_NAME = "Allocated Project Name" |
| 17 | +CF_ATTR_ALLOCATED_PROJECT_ID = "Allocated Project ID" |
| 18 | +CF_ATTR_INSTITUTION_SPECIFIC_CODE = "Institution-Specific Code" |
17 | 19 |
|
18 | 20 |
|
19 | 21 | @dataclass |
@@ -49,58 +51,69 @@ def coldfront_client(self): |
49 | 51 | session.headers.update(headers) |
50 | 52 | return session |
51 | 53 |
|
52 | | - def _get_project_list(self): |
53 | | - return self.data[invoice.PROJECT_FIELD].unique() |
| 54 | + def _get_project_id_list(self): |
| 55 | + """Returns list of project IDs from billable clusters""" |
| 56 | + nonbillable_cluster_mask = ~self.data[invoice.CLUSTER_NAME_FIELD].isin( |
| 57 | + validate_billable_pi_processor.NONBILLABLE_CLUSTERS |
| 58 | + ) |
| 59 | + return self.data[nonbillable_cluster_mask][invoice.PROJECT_ID_FIELD].unique() |
54 | 60 |
|
55 | | - def _fetch_coldfront_allocation_api(self, allocation_list): |
| 61 | + def _fetch_coldfront_allocation_api(self): |
56 | 62 | coldfront_api_url = os.environ.get( |
57 | 63 | "COLDFRONT_URL", "https://coldfront.mss.mghpcc.org/api/allocations" |
58 | 64 | ) |
59 | | - api_query_str = "&".join( |
60 | | - [ |
61 | | - f"attr_{CF_ATTR_ALLOCATED_PROJECT_NAME}={project}" |
62 | | - for project in allocation_list |
63 | | - ] |
64 | | - ) |
65 | | - r = self.coldfront_client.get(f"{coldfront_api_url}?{api_query_str}") |
| 65 | + r = self.coldfront_client.get(f"{coldfront_api_url}?all=true") |
66 | 66 |
|
67 | 67 | return r.json() |
68 | 68 |
|
69 | 69 | def _get_allocation_data(self, coldfront_api_data): |
| 70 | + """Returns a mapping of project IDs to a dict of project name, PI name, and institution code.""" |
70 | 71 | allocation_data = {} |
71 | | - for project, project_dict in coldfront_api_data.items(): |
72 | | - allocation_data[project] = { |
73 | | - invoice.PI_FIELD: project_dict["project"]["pi"], |
74 | | - invoice.INSTITUTION_ID_FIELD: project_dict["attributes"][ |
75 | | - "Institution-Specific Code" |
76 | | - ], |
77 | | - } |
| 72 | + for project_dict in coldfront_api_data: |
| 73 | + try: |
| 74 | + # Allow allocation to not have institute code |
| 75 | + project_id = project_dict["attributes"][CF_ATTR_ALLOCATED_PROJECT_ID] |
| 76 | + project_name = project_dict["attributes"][ |
| 77 | + CF_ATTR_ALLOCATED_PROJECT_NAME |
| 78 | + ] |
| 79 | + pi_name = project_dict["project"]["pi"] |
| 80 | + institute_code = project_dict["attributes"].get( |
| 81 | + CF_ATTR_INSTITUTION_SPECIFIC_CODE, "N/A" |
| 82 | + ) |
| 83 | + allocation_data[project_id] = { |
| 84 | + invoice.PROJECT_FIELD: project_name, |
| 85 | + invoice.PI_FIELD: pi_name, |
| 86 | + invoice.INSTITUTION_ID_FIELD: institute_code, |
| 87 | + } |
| 88 | + except KeyError: |
| 89 | + continue |
| 90 | + |
78 | 91 | return allocation_data |
79 | 92 |
|
80 | 93 | def _validate_allocation_data(self, allocation_data): |
81 | 94 | missing_projects = ( |
82 | | - set(self._get_project_list()) |
| 95 | + set(self._get_project_id_list()) |
83 | 96 | - set(allocation_data.keys()) |
84 | 97 | - set(self.nonbillable_projects) |
85 | 98 | ) |
86 | 99 | missing_projects = list(missing_projects) |
87 | 100 | missing_projects.sort() # Ensures order for testing purposes |
88 | 101 | if missing_projects: |
89 | | - logger.warning( |
| 102 | + raise ValueError( |
90 | 103 | f"Projects {missing_projects} not found in Coldfront and are billable! Please check the project names" |
91 | 104 | ) |
92 | 105 |
|
93 | 106 | def _apply_allocation_data(self, allocation_data): |
94 | | - for project, data in allocation_data.items(): |
95 | | - mask = self.data[invoice.PROJECT_FIELD] == project |
| 107 | + for project_id, data in allocation_data.items(): |
| 108 | + mask = self.data[invoice.PROJECT_ID_FIELD] == project_id |
| 109 | + self.data.loc[mask, invoice.PROJECT_FIELD] = data[invoice.PROJECT_FIELD] |
96 | 110 | self.data.loc[mask, invoice.PI_FIELD] = data[invoice.PI_FIELD] |
97 | 111 | self.data.loc[mask, invoice.INSTITUTION_ID_FIELD] = data[ |
98 | 112 | invoice.INSTITUTION_ID_FIELD |
99 | 113 | ] |
100 | 114 |
|
101 | 115 | def _process(self): |
102 | | - project_allocations_list = self._get_project_list() |
103 | | - api_data = self._fetch_coldfront_allocation_api(project_allocations_list) |
| 116 | + api_data = self._fetch_coldfront_allocation_api() |
104 | 117 | allocation_data = self._get_allocation_data(api_data) |
105 | 118 | self._validate_allocation_data(allocation_data) |
106 | 119 | self._apply_allocation_data(allocation_data) |
0 commit comments